///|
pub(all) enum Side {
  Left
  Right
  Top
  Bottom
} derive(Eq, Debug)

///|
pub(all) enum Axis {
  Row
  Column
} derive(Eq, Debug)

///|
pub struct Group {
  id : String
  label : String
  parent : String?
  icon : @icons.Reference?
} derive(Eq, Debug)

///|
pub fn Group::new(
  id : String,
  label~ : String = id,
  parent? : String,
  icon? : @icons.Reference,
) -> Group {
  { id, label, parent, icon, }
}

///|
pub struct Service {
  id : String
  label : String
  parent : String?
  icon : @icons.Reference?
  icon_text : String?
} derive(Eq, Debug)

///|
pub fn Service::new(
  id : String,
  label~ : String = id,
  parent? : String,
  icon? : @icons.Reference,
  icon_text? : String,
) -> Service {
  { id, label, parent, icon, icon_text, }
}

///|
pub struct Junction {
  id : String
  parent : String?
} derive(Eq, Debug)

///|
pub fn Junction::new(id : String, parent? : String) -> Junction {
  { id, parent, }
}

///|
pub struct Endpoint {
  id : String
  side : Side
  group : Bool
} derive(Eq, Debug)

///|
pub fn Endpoint::new(
  id : String,
  side : Side,
  group~ : Bool = false,
) -> Endpoint {
  { id, side, group, }
}

///|
pub struct Connection {
  id : String
  source : Endpoint
  target : Endpoint
  label : String?
  source_head : Bool
  target_head : Bool
} derive(Eq, Debug)

///|
pub fn Connection::new(
  id : String,
  source : Endpoint,
  target : Endpoint,
  label? : String,
  source_head~ : Bool = false,
  target_head~ : Bool = false,
) -> Connection {
  { id, source, target, label, source_head, target_head, }
}

///|
pub struct Alignment {
  axis : Axis
  members : ReadOnlyArray[String]
} derive(Eq, Debug)

///|
pub fn Alignment::new(axis : Axis, members : ArrayView[String]) -> Alignment {
  { axis, members: ReadOnlyArray::from_array(members), }
}

///|
pub struct Architecture {
  id : String
  groups : ReadOnlyArray[Group]
  services : ReadOnlyArray[Service]
  junctions : ReadOnlyArray[Junction]
  connections : ReadOnlyArray[Connection]
  alignments : ReadOnlyArray[Alignment]
  title : String?
  accessibility_title : String?
  accessibility_description : String?
} derive(Eq, Debug)

///|
pub fn Architecture::new(
  id : String,
  services : ArrayView[Service],
  connections : ArrayView[Connection],
  groups~ : ArrayView[Group] = [],
  junctions~ : ArrayView[Junction] = [],
  alignments~ : ArrayView[Alignment] = [],
  title? : String,
  accessibility_title? : String,
  accessibility_description? : String,
) -> Architecture {
  {
    id,
    services: ReadOnlyArray::from_array(services),
    connections: ReadOnlyArray::from_array(connections),
    groups: ReadOnlyArray::from_array(groups),
    junctions: ReadOnlyArray::from_array(junctions),
    alignments: ReadOnlyArray::from_array(alignments),
    title,
    accessibility_title,
    accessibility_description,
  }
}

///|
pub fn validate(
  model : Architecture,
  nodes_limit~ : Int32 = 10000,
  edges_limit~ : Int32 = 50000,
  depth_limit~ : Int32 = 256,
  work_limit~ : Int32 = 1000000,
) -> ReadOnlyArray[@diagrams.Diagnostic] {
  let out = []
  let ids : Map[String, Bool] = Map([])
  let parents : Map[String, String?] = Map([])
  let groups : Map[String, Group] = Map([])
  let nodes : Map[String, Bool] = Map([])
  let mut work = 0
  let mut exceeded = false
  fn error(code : String, text : String) {
    if out.length() < 256 {
      out.push(@diagrams.Diagnostic::error("architecture/" + code, text))
    }
  }
  fn charge(n : Int32) -> Bool {
    if exceeded {
      return false
    }
    if n < 0 || n > work_limit - work {
      exceeded = true
      error("work-limit", "Architecture validation work budget exhausted")
      false
    } else {
      work += n
      true
    }
  }
  fn add(id : String, parent : String?) {
    if !charge(id.length() + 1) {
      return
    }
    if id == "" || ids.contains(id) {
      error("duplicate-id", "Ambiguous element ID: " + id)
    }
    ids[id] = true
    parents[id] = parent
  }
  if nodes_limit < 0 ||
    edges_limit < 0 ||
    depth_limit < 0 ||
    work_limit < 0 ||
    model.groups.length() + model.services.length() + model.junctions.length() >
    nodes_limit ||
    model.connections.length() > edges_limit ||
    model.alignments.length() > nodes_limit {
    error("model-limit", "Architecture model size budget exhausted")
    return ReadOnlyArray::from_array(out[:])
  }
  if !charge(
      model.id.length() +
      model.title.map(v => v.length()).unwrap_or(0) +
      model.accessibility_title.map(v => v.length()).unwrap_or(0) +
      model.accessibility_description.map(v => v.length()).unwrap_or(0),
    ) {
    return ReadOnlyArray::from_array(out[:])
  }
  for g in model.groups {
    if !charge(
        g.label.length() +
        g.icon.map(i => i.pack.length() + i.key.length()).unwrap_or(0),
      ) {
      break
    }
    add(g.id, g.parent)
    groups[g.id] = g
  }
  for n in model.services {
    if !charge(
        n.label.length() +
        n.icon_text.map(v => v.length()).unwrap_or(0) +
        n.icon.map(i => i.pack.length() + i.key.length()).unwrap_or(0),
      ) {
      break
    }
    add(n.id, n.parent)
    nodes[n.id] = true
  }
  for n in model.junctions {
    add(n.id, n.parent)
    nodes[n.id] = true
  }
  for id, parent in parents {
    let mut next = parent
    let seen : Map[String, Bool] = Map([])
    let mut depth = 0
    while next is Some(p) {
      if !charge(1) {
        break
      }
      if !groups.contains(p) {
        error("invalid-parent", "Missing group " + p + " for " + id)
        break
      }
      if p == id || seen.contains(p) {
        error("ownership-cycle", "Ownership cycle at " + id)
        break
      }
      seen[p] = true
      depth += 1
      if depth > depth_limit {
        error("depth-limit", "Ownership depth budget exhausted at " + id)
        break
      }
      next = parents.get(p).unwrap_or(None)
    }
  }
  let edges : Map[String, Bool] = Map([])
  for edge in model.connections {
    if (edge.source.group || edge.target.group) &&
      parents.get(edge.source.id).unwrap_or(None) != None &&
      parents.get(edge.source.id) == parents.get(edge.target.id) {
      error(
        "invalid-group-attachment",
        "Group-boundary connection does not cross groups: " + edge.id,
      )
    }
    if !charge(
        1 + edge.id.length() + edge.label.map(v => v.length()).unwrap_or(0),
      ) {
      break
    }
    if edge.id == "" || edges.contains(edge.id) {
      error("duplicate-connection", "Ambiguous connection ID: " + edge.id)
    }
    edges[edge.id] = true
    for endpoint in [edge.source, edge.target] {
      if !nodes.contains(endpoint.id) {
        error("invalid-endpoint", "Missing service or junction: " + endpoint.id)
      }
      if endpoint.group && parents.get(endpoint.id).unwrap_or(None) == None {
        error(
          "invalid-group-attachment",
          "No containing group for " + endpoint.id,
        )
      }
    }
  }
  for align in model.alignments {
    if !charge(align.members.length()) {
      break
    }
    if align.members.length() < 2 {
      error("invalid-alignment", "Alignment requires at least two members")
    }
    let seen : Map[String, Bool] = Map([])
    for id in align.members {
      if !nodes.contains(id) || seen.contains(id) {
        error("invalid-alignment", "Invalid alignment member: " + id)
      }
      seen[id] = true
    }
  }
  ReadOnlyArray::from_array(out[:])
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

///|
pub fn Side::to_string(self : Side) -> String {
  match self {
    Left => "L"
    Right => "R"
    Top => "T"
    Bottom => "B"
  }
}