///|
pub struct Conversion {
  sequence : @model.Sequence?
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
} derive(Eq, Debug)

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

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

///|
fn lower(s : String) -> String {
  @icons.normalize_key(s)
}

///|
fn kind(key : String) -> @model.ParticipantKind {
  match lower(key) {
    "actor" => Actor
    "boundary" => Boundary
    "control" => Control
    "entity" => Entity
    "database" => Database
    "collections" => Collections
    "queue" => Queue
    _ => Participant
  }
}

///|
fn formatted(value : String) -> String {
  let out = StringBuilder()
  let units = value.to_array()
  let mut pending = false
  let mut suppress_space = true
  let mut quote : Char? = None
  let mut escaped = false
  let mut index = 0
  while index < units.length() {
    let c = units[index]
    if quote is Some(delimiter) {
      out.write_char(c)
      if escaped {
        escaped = false
      } else if c == '\\' {
        escaped = true
      } else if c == delimiter {
        if index + 1 < units.length() && units[index + 1] == delimiter {
          // Native CSTRING uses doubled quote delimiters inside a literal.
          index += 1
          out.write_char(units[index])
        } else {
          quote = None
        }
      }
      index += 1
      continue
    }
    if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
      pending = true
      index += 1
      continue
    }
    let punctuation = [',', ';', '.', '(', ')'].contains(c)
    if pending && !punctuation && !suppress_space {
      out.write_char(' ')
    }
    pending = false
    out.write_char(c)
    if c == '"' || c == '\'' || c == '`' {
      quote = Some(c)
    }
    suppress_space = [',', ';', '.', '('].contains(c)
    index += 1
  }
  out.to_string()
}

///|
fn assignment_label(value : String) -> String {
  let words = value.split(" ").filter(s => s != "").to_array()
  words.last().map(s => s.to_owned()).unwrap_or(value)
}

///|
fn creation_label(operation : String) -> String {
  let value = operation.trim().to_owned()
  let value = if value.has_prefix("(") && value.has_suffix(")") {
    value[1:value.length() - 1].to_owned()
  } else {
    value
  }
  "«" +
  (if value.trim() == "" {
    "create"
  } else {
    formatted(value).trim().to_owned()
  }) +
  "»"
}

///|
fn statement_units(kind : @ast.StatementKind) -> Int32 {
  match kind {
    Participant(id~, label~, annotator~) =>
      id.length() +
      label.map(s => s.length()).unwrap_or(0) +
      annotator.map(s => s.length()).unwrap_or(0)
    Call(sender~, receiver~, operation~, assignment~, ..) =>
      sender.map(s => s.length()).unwrap_or(0) +
      receiver.length() +
      operation.length() +
      assignment.map(s => s.length()).unwrap_or(0)
    Async(sender~, receiver~, label~, ..) =>
      sender.length() + receiver.length() + label.length()
    Starter(text)
    | Return(text)
    | Comment(text)
    | Title(text)
    | Metadata(text) => text.length()
    Begin(keyword~, expression~) | Section(keyword~, expression~) =>
      keyword.length() + expression.length()
    End => 0
  }
}

///|
/// Convert explicit native scopes to ordered calls and balanced activations.
pub fn convert(
  diagram : @ast.Diagram,
  limits~ : @syntax.Limits = @syntax.Limits::new(),
) -> Conversion {
  let diagnostics = []
  let participants : Array[@model.Participant] = []
  let ids : Map[String, Int32] = Map([])
  let ends : Map[Int32, Int32] = Map([])
  let stack = []
  let mut title = None
  let mut work = 0
  let mut count = 0
  let mut starter = "_STARTER_"
  let mut failed = false
  let diagnostic_cap = if limits.diagnostics > 0 {
    limits.diagnostics
  } else {
    1
  }
  fn error(code : String, message : String, span : @diagrams.Span?) {
    failed = true
    if diagnostics.length() < diagnostic_cap {
      diagnostics.push(
        @diagrams.Diagnostic::error("zenuml/" + code, message, span?),
      )
    } else {
      diagnostics[diagnostics.length() - 1] = @diagrams.Diagnostic::error(
        "zenuml/diagnostic-limit",
        "Diagnostic limit exceeded",
        span?,
      )
    }
  }
  fn charge(n : Int32) -> Bool {
    if n < 0 || n > limits.work - work {
      error("work-limit", "Conversion work limit exceeded", None)
      false
    } else {
      work += n
      true
    }
  }
  fn ensure(
    id : String,
    label : String?,
    annotator : String?,
    declared : Bool,
    span : @diagrams.Span?,
  ) -> Unit {
    if annotator is Some(key) &&
      !@icons.known(@icons.Reference::new("zenuml", key)) {
      if diagnostics.length() >= diagnostic_cap {
        error("diagnostic-limit", "Diagnostic limit exceeded", span)
        return
      }
      diagnostics.push(
        @diagrams.Diagnostic::warning(
          "zenuml/unknown-annotator",
          "Unknown participant annotator " + key + " on " + id,
          span?,
        ),
      )
    }
    if ids.get(id) is Some(index) {
      if declared {
        let old = participants[index]
        if old.icon != None &&
          old.icon !=
          annotator.map(key => @icons.Reference::new("zenuml", lower(key))) {
          error(
            "participant-conflict",
            "Conflicting participant declaration: " + id,
            None,
          )
        } else {
          participants[index] = @model.Participant::new(
            id,
            label=label.unwrap_or(old.label),
            kind=annotator.map(kind).unwrap_or(old.kind),
            icon?=match annotator {
              Some(key) => Some(@icons.Reference::new("zenuml", lower(key)))
              None => old.icon
            },
          )
        }
      }
    } else {
      if participants.length() >= limits.participants {
        error("participant-limit", "Participant count limit exceeded", None)
        return
      }
      ids[id] = participants.length()
      participants.push(
        @model.Participant::new(
          id,
          label?,
          kind=if id == "_STARTER_" {
            Actor
          } else {
            annotator.map(kind).unwrap_or(Participant)
          },
          icon?=annotator.map(key => @icons.Reference::new("zenuml", lower(key))),
        ),
      )
    }
  }
  fn append(out : Array[@model.Event], event : @model.Event) {
    count += 1
    if count > limits.events {
      error("event-limit", "Converted event count limit exceeded", None)
    } else {
      out.push(event)
    }
  }
  if limits.depth < 0 ||
    limits.depth > 256 ||
    limits.events < 0 ||
    limits.participants < 0 ||
    limits.work < 0 ||
    limits.diagnostics < 1 {
    error("limits", "Invalid conversion limits", None)
    return {
      sequence: None,
      diagnostics: ReadOnlyArray::from_array(diagnostics),
    }
  }
  let mut need_starter = !diagram.statements
    .iter()
    .any(statement => {
      match statement.kind {
        Participant(..) | Call(..) | Async(..) => true
        _ => false
      }
    })
  let mut call_depth = 0
  let nesting : Array[Bool] = []
  for statement in diagram.statements {
    match statement.kind {
      Starter(id) => starter = id
      Call(sender~, scope~, ..) => {
        if sender == None && call_depth == 0 {
          need_starter = true
        }
        if scope {
          nesting.push(true)
          call_depth += 1
        }
      }
      Begin(..) | Section(..) => nesting.push(false)
      End => if nesting.pop() is Some(true) { call_depth -= 1 }
      _ => ()
    }
  }
  if need_starter &&
    starter == "_STARTER_" &&
    diagram.statements
    .iter()
    .any(statement => statement.kind is Participant(id="_STARTER_", ..)) {
    error(
      "starter-collision",
      "Implicit starter identity collides with a declaration",
      None,
    )
  }
  if need_starter && starter == "_STARTER_" {
    ensure(starter, None, Some("actor"), false, None)
  }
  // Resolve declarations first, so aliases and symbols apply to every occurrence.
  for i, statement in diagram.statements {
    if !charge(1 + statement_units(statement.kind)) {
      break
    }
    match statement.kind {
      Participant(id~, label~, annotator~) =>
        ensure(id, label, annotator, true, Some(statement.span))
      Title(value) => title = Some(value)
      Starter(_) => ()
      Begin(..) | Section(..) | Call(scope=true, ..) => {
        stack.push(i)
        if stack.length() > limits.depth {
          error(
            "depth-limit",
            "Conversion depth limit exceeded",
            Some(statement.span),
          )
          break
        }
      }
      End =>
        match stack.pop() {
          Some(begin) => ends[begin] = i
          None => error("nesting", "Unmatched close", Some(statement.span))
        }
      _ => ()
    }
  }
  if stack.length() > 0 {
    error(
      "missing-brace",
      "Unclosed scope",
      Some(diagram.statements[stack[0]].span),
    )
  }
  fn events(
    start : Int32,
    end : Int32,
    caller : String?,
    targets : ArrayView[String],
    depth : Int32,
  ) -> Array[@model.Event] {
    let out = []
    let mut i = start
    let comments : Array[String] = []
    if depth > limits.depth {
      error("depth-limit", "Conversion depth limit exceeded", None)
      return out
    }
    fn note(people : ArrayView[String]) {
      if comments.length() > 0 {
        append(out, Note(@model.Note::new(Over, people, comments.join("\n"))))
        comments.clear()
      }
    }
    while i < end && !failed {
      if !charge(1) {
        break
      }
      let statement = diagram.statements[i]
      match statement.kind {
        Comment(text) => comments.push(text)
        Participant(..) | Title(_) | Starter(_) => comments.clear()
        Metadata(value) => {
          comments.clear()
          if diagnostics.length() < diagnostic_cap {
            diagnostics.push(
              @diagrams.Diagnostic::warning(
                "zenuml/presentation-metadata",
                "Source metadata is retained without changing library rendering: " +
                value,
                span=statement.span,
              ),
            )
          } else {
            error(
              "diagnostic-limit",
              "Diagnostic limit exceeded",
              Some(statement.span),
            )
          }
        }
        Async(sender~, receiver~, label~, reply~) => {
          ensure(sender, None, None, false, Some(statement.span))
          ensure(receiver, None, None, false, Some(statement.span))
          note([sender, receiver])
          append(
            out,
            Message(
              @model.Message::new(
                sender,
                receiver,
                if reply {
                  formatted(label).trim().to_owned()
                } else {
                  label
                },
                line=if reply { Dotted } else { Solid },
                receiver_head=if reply { OpenArrow } else { Async },
              ),
            ),
          )
        }
        Return(label) =>
          match (caller, targets.last()) {
            (Some(from), Some(to)) => {
              note([from, to])
              append(
                out,
                Message(
                  @model.Message::new(
                    from,
                    to,
                    formatted(label).trim().to_owned(),
                    line=Dotted,
                    receiver_head=OpenArrow,
                  ),
                ),
              )
            }
            _ =>
              error(
                "return-target",
                "Return has no enclosing caller",
                Some(statement.span),
              )
          }
        Call(sender~, receiver~, operation~, assignment~, creation~, scope~) => {
          let from = sender.unwrap_or(caller.unwrap_or(starter))
          if from == "_STARTER_" &&
            ids.contains(from) &&
            participants[ids[from]].kind != Actor {
            error(
              "starter-collision",
              "Implicit starter identity collides with a declaration",
              Some(statement.span),
            )
          }
          ensure(from, None, None, false, Some(statement.span))
          ensure(receiver, None, None, false, Some(statement.span))
          note([from, receiver])
          if creation {
            append(out, Create(receiver))
          }
          append(
            out,
            Message(
              @model.Message::new(
                from,
                receiver,
                if creation {
                  creation_label(operation)
                } else {
                  formatted(operation).trim().to_owned()
                },
              ),
            ),
          )
          append(out, Activate(receiver))
          if scope {
            let close = ends.get(i).unwrap_or(end)
            let next = targets.to_owned()
            next.push(from)
            out.append(events(i + 1, close, Some(receiver), next, depth + 1))
            i = close
          }
          if assignment is Some(value) && from != receiver {
            append(
              out,
              Message(
                @model.Message::new(
                  receiver,
                  from,
                  assignment_label(value),
                  line=Dotted,
                  receiver_head=OpenArrow,
                ),
              ),
            )
          }
          append(out, Deactivate(receiver))
        }
        Begin(keyword~, expression~) => {
          let sections : Array[@model.Section] = []
          let close = ends.get(i).unwrap_or(end)
          let role = if keyword == "try" {
            @model.SectionRole::Try
          } else {
            Plain
          }
          let body = if keyword == "par" {
            []
          } else {
            events(i + 1, close, caller, targets, depth + 1)
          }
          if keyword == "par" {
            let mut begin = i + 1
            while begin < close && !failed {
              let mut statement_at = begin
              while statement_at < close &&
                    diagram.statements[statement_at].kind is Comment(_) {
                statement_at += 1
              }
              if statement_at >= close {
                break
              }
              let native = diagram.statements[statement_at].kind
              let mut finish = match native {
                Call(scope=true, ..) | Begin(..) =>
                  ends.get(statement_at).unwrap_or(close) + 1
                _ => statement_at + 1
              }
              let chained = match native {
                Begin(keyword="if", ..) => "alt"
                Begin(keyword="try", ..) => "try"
                _ => ""
              }
              while finish < close && chained != "" {
                let mut look = finish
                while look < close &&
                      diagram.statements[look].kind is Comment(_) {
                  look += 1
                }
                if look >= close {
                  break
                }
                match diagram.statements[look].kind {
                  Section(keyword=section_word, ..) =>
                    if (
                        chained == "alt" &&
                        ["else", "else if"].contains(section_word)
                      ) ||
                      (
                        chained == "try" &&
                        ["catch", "finally"].contains(section_word)
                      ) {
                      finish = ends.get(look).unwrap_or(close) + 1
                    } else {
                      break
                    }
                  _ => break
                }
              }
              let branch = events(begin, finish, caller, targets, depth + 1)
              if !branch.is_empty() {
                sections.push(@model.Section::new(branch))
              }
              begin = finish
            }
            if sections.is_empty() {
              sections.push(@model.Section::new([]))
            }
          } else {
            sections.push(
              @model.Section::new(
                body,
                label?=if expression == "" { None } else { Some(expression) },
                role~,
              ),
            )
          }
          let frame_kind = match keyword {
            "if" => @model.FrameKind::Alt
            "opt" => Opt
            "par" => Par
            "try" => Try
            "break" => Break
            _ => Loop
          }
          let mut next = close + 1
          while next < end {
            let mut look = next
            let section_comments = []
            while look < end {
              match diagram.statements[look].kind {
                Comment(text) => {
                  section_comments.push(text)
                  look += 1
                }
                _ => break
              }
            }
            if look >= end {
              break
            }
            match diagram.statements[look].kind {
              Section(keyword=section_word, expression=label) => {
                let valid = (
                    frame_kind == Alt &&
                    ["else", "else if"].contains(section_word)
                  ) ||
                  (
                    frame_kind == Try &&
                    ["catch", "finally"].contains(section_word)
                  )
                if !valid {
                  break
                }
                let finish = ends.get(look).unwrap_or(end)
                let contents = events(
                  look + 1,
                  finish,
                  caller,
                  targets,
                  depth + 1,
                )
                if !section_comments.is_empty() {
                  contents.insert(
                    0,
                    Note(
                      @model.Note::new(
                        Over,
                        participants.map(p => p.id),
                        section_comments.join("\n"),
                      ),
                    ),
                  )
                }
                sections.push(
                  @model.Section::new(
                    contents,
                    label?=if label == "" { None } else { Some(label) },
                    role=if section_word == "catch" {
                      Catch
                    } else if section_word == "finally" {
                      Finally
                    } else {
                      Plain
                    },
                  ),
                )
                next = finish + 1
              }
              _ => break
            }
          }
          let people = participants.map(p => p.id)
          note(people)
          append(
            out,
            Frame(@model.Frame::new(frame_kind, sections[0], rest=sections[1:])),
          )
          i = next - 1
        }
        Section(..) =>
          error(
            "section",
            "Section has no matching frame",
            Some(statement.span),
          )
        End =>
          error("nesting", "Unexpected closing scope", Some(statement.span))
      }
      i += 1
    }
    out
  }
  let output = if failed {
    []
  } else {
    events(0, diagram.statements.length(), None, [], 0)
  }
  {
    sequence: if failed {
      None
    } else {
      Some(@model.Sequence::new(participants, output, title?))
    },
    diagnostics: ReadOnlyArray::from_array(diagnostics),
  }
}

///|
/// Stable diagnostic codes emitted by this processing stage.
pub let diagnostic_codes : ReadOnlyArray[String] = [
  "zenuml/depth-limit", "zenuml/diagnostic-limit", "zenuml/event-limit", "zenuml/limits",
  "zenuml/missing-brace", "zenuml/nesting", "zenuml/participant-conflict", "zenuml/participant-limit",
  "zenuml/presentation-metadata", "zenuml/return-target", "zenuml/section", "zenuml/starter-collision",
  "zenuml/unknown-annotator", "zenuml/work-limit",
]