///|
pub struct Conversion {
  architecture : @model.Architecture?
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}

///|
pub let diagnostic_codes : ReadOnlyArray[String] = [
  "architecture/duplicate-id", "architecture/duplicate-connection", "architecture/invalid-parent",
  "architecture/ownership-cycle", "architecture/depth-limit", "architecture/invalid-endpoint",
  "architecture/invalid-group-attachment", "architecture/invalid-alignment", "architecture/model-limit",
  "architecture/work-limit", "architecture/metadata-option",
]

///|
pub fn to_architecture(
  diagram : @ast.Diagram,
  limits~ : @syntax.Limits = @syntax.Limits::new(),
  id~ : String = "architecture",
) -> Conversion {
  let spans : Map[String, @diagrams.Span] = Map([])
  let alignment_spans : Map[String, @diagrams.Span] = Map([])
  let groups = []
  let services = []
  let junctions = []
  let connections = []
  let alignments = []
  let mut title = None
  let mut accessibility_title = None
  let mut accessibility_description = None
  let diagnostics = []
  let mut work = 0
  let mut node_count = 0
  let mut edge_count = 0
  let mut alignment_count = 0
  for statement in diagram.statements {
    work += 1
    match statement.kind {
      Group(id~, icon~, label~, parent~) => {
        node_count += 1
        work += id.length() +
          icon.map(v => v.length()).unwrap_or(0) +
          label.map(v => v.length()).unwrap_or(0) +
          parent.map(v => v.length()).unwrap_or(0)
      }
      Service(id~, icon~, icon_text~, label~, parent~) => {
        node_count += 1
        work += id.length() +
          icon.map(v => v.length()).unwrap_or(0) +
          icon_text.map(v => v.length()).unwrap_or(0) +
          label.map(v => v.length()).unwrap_or(0) +
          parent.map(v => v.length()).unwrap_or(0)
      }
      Junction(id~, parent~) => {
        node_count += 1
        work += id.length() + parent.map(v => v.length()).unwrap_or(0)
      }
      Edge(source~, target~, label~, ..) => {
        edge_count += 1
        work += source.id.length() +
          target.id.length() +
          label.map(v => v.length()).unwrap_or(0)
      }
      Alignment(members~, ..) => {
        alignment_count += 1
        work += members.length()
        for name in members {
          work += name.length()
          if work > limits.work {
            break
          }
        }
      }
      Title(v) | AccTitle(v) | AccDescription(v) | Comment(v) =>
        work += v.length()
    }
    let code = if work > limits.work {
      Some("work-limit")
    } else if node_count > limits.nodes {
      Some("node-limit")
    } else if edge_count > limits.edges {
      Some("edge-limit")
    } else if alignment_count > limits.alignments {
      Some("alignment-limit")
    } else {
      None
    }
    if code is Some(code) {
      return {
        architecture: None,
        diagnostics: [
          @diagrams.Diagnostic::error(
            "architecture/" + code,
            "Conversion budget exhausted",
            span=statement.span,
          ),
        ],
      }
    }
  }
  for statement in diagram.statements {
    match statement.kind {
      Group(id~, ..) | Service(id~, ..) | Junction(id~, ..) =>
        spans[id] = statement.span
      Edge(..) => spans["edge-\{connections.length()}"] = statement.span
      Alignment(members~, ..) =>
        for name in members {
          alignment_spans[name] = statement.span
        }
      _ => ()
    }
    match statement.kind {
      Group(id~, icon~, label~, parent~) => {
        let icon = icon.map(key => @icons.Reference::new("architecture", key))
        groups.push(@model.Group::new(id, label?, parent?, icon?))
      }
      Service(id~, icon~, icon_text~, label~, parent~) => {
        let icon = icon.map(key => @icons.Reference::new("architecture", key))
        services.push(
          @model.Service::new(id, label?, parent?, icon?, icon_text?),
        )
      }
      Junction(id~, parent~) =>
        junctions.push(@model.Junction::new(id, parent?))
      Edge(source~, target~, label~, source_head~, target_head~) =>
        connections.push(
          @model.Connection::new(
            "edge-\{connections.length()}",
            source,
            target,
            label?,
            source_head~,
            target_head~,
          ),
        )
      Alignment(axis~, members~) =>
        alignments.push(
          @model.Alignment::new(axis, members.iter().to_array()[:]),
        )
      Title(v) => title = Some(v)
      AccTitle(v) => accessibility_title = Some(v)
      AccDescription(v) => accessibility_description = Some(v)
      Comment(_) => ()
    }
  }
  let model = @model.Architecture::new(
    id,
    services[:],
    connections[:],
    groups=groups[:],
    junctions=junctions[:],
    alignments=alignments[:],
    title?,
    accessibility_title?,
    accessibility_description?,
  )
  let declared_groups : Map[String, Bool] = Map([])
  for statement in diagram.statements {
    match statement.kind {
      Group(id~, parent~, ..) => {
        if parent is Some(parent) && !declared_groups.contains(parent) {
          diagnostics.push(
            @diagrams.Diagnostic::error(
              "architecture/invalid-parent",
              "Group parent must be declared before child: " + parent,
              span=statement.span,
            ),
          )
        }
        declared_groups[id] = true
      }
      _ => ()
    }
  }
  let parents : Map[String, String?] = Map([])
  for service in services {
    parents[service.id] = service.parent
  }
  for junction in junctions {
    parents[junction.id] = junction.parent
  }
  for statement in diagram.statements {
    if statement.kind is Edge(source~, target~, ..) &&
      (source.group || target.group) {
      let a = parents.get(source.id).unwrap_or(None)
      let b = parents.get(target.id).unwrap_or(None)
      if a != None && a == b {
        diagnostics.push(
          @diagrams.Diagnostic::error(
            "architecture/invalid-group-attachment",
            "Group-boundary connection must cross groups",
            span=statement.span,
          ),
        )
      }
    }
  }
  for
    finding in @model.validate(
      model,
      nodes_limit=limits.nodes,
      edges_limit=limits.edges,
      depth_limit=limits.depth,
      work_limit=limits.work,
    ) {
    let key = match finding.message.rev_find(" ") {
      Some(i) => finding.message[i + 1:].to_owned()
      None => finding.message
    }
    let span = if finding.code == "architecture/invalid-alignment" {
      alignment_spans.get(key)
    } else {
      spans.get(key)
    }
    diagnostics.push({ ..finding, span, })
  }
  if diagram.front_matter != None || !diagram.directives.is_empty() {
    diagnostics.push(
      @diagrams.Diagnostic::warning(
        "architecture/metadata-option", "Source metadata is retained; layout follows caller options",
      ),
    )
  }
  if diagnostics.length() > limits.diagnostics.max(1) {
    diagnostics.truncate(limits.diagnostics.max(1))
    diagnostics[diagnostics.length() - 1] = @diagrams.Diagnostic::error(
      "architecture/diagnostic-limit", "Conversion diagnostic budget exhausted",
    )
  }
  {
    architecture: if diagnostics.iter().any(d => d.severity == Error) {
      None
    } else {
      Some(model)
    },
    diagnostics: ReadOnlyArray::from_array(diagnostics[:]),
  }
}