///|
/// Flowcharts share vector geometry primitives with the swimlane layout engine.
/// The adapter preserves ordinary groups and introduces no responsibility lanes.
pub struct Limits {
  nodes : Int32
  edges : Int32
  depth : Int32
  work : Int32
  output_units : Int32
  width : Int32
  height : Int32
  minimum_approach : Double
} derive(Eq, Debug)

///|
pub fn Limits::new(
  nodes~ : Int32 = 10000,
  edges~ : Int32 = 50000,
  depth~ : Int32 = 256,
  work~ : Int32 = 1000000,
  output_units~ : Int32 = 16777216,
  width~ : Int32 = 1000000,
  height~ : Int32 = 1000000,
  minimum_approach~ : Double = 24.0,
) -> Limits {
  { minimum_approach, nodes, edges, depth, work, output_units, width, height, }
}

///|
pub struct Connection {
  connection : @model.Connection
  index : Int32
  source : String
  target : String
  line : ReadOnlyArray[@geometry.Point]
  label : @geometry.Label?
} derive(Eq, Debug)

///|
pub struct Geometry {
  view : @model.Flowchart
  shapes : ReadOnlyArray[@geometry.Shape]
  connections : ReadOnlyArray[Connection]
  width : Int32
  height : Int32
  priv drawing : @geometry.Geometry
  priv drawing_result : @geometry.Result
  priv prior_diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
  priv intentional : ReadOnlyArray[(String, String)]
  priv hidden_containers : ReadOnlyArray[String]
} derive(Eq, Debug)

///|
pub struct Result {
  geometry : Geometry?
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
} derive(Eq, Debug)

///|
fn vector_direction(value : @model.Direction) -> @vector.Direction {
  match value {
    TB => TB
    BT => BT
    LR => LR
    RL => RL
  }
}

///|
fn vector_label(value : @model.LabelKind) -> @vector.LabelKind {
  match value {
    Text => Text
    Markdown => Markdown
    Html => Html
  }
}

///|
fn vector_marker(value : @model.Marker) -> @vector.Marker {
  match value {
    None => None
    Arrow => Arrow
    Circle => Circle
    Cross => Cross
  }
}

///|
fn vector_style(value : @model.LineStyle) -> @vector.LineStyle {
  match value {
    Solid => Solid
    Dotted => Dotted
    Thick => Thick
    Invisible => Invisible
  }
}

///|
pub fn diagnostics(
  values : ArrayView[@diagrams.Diagnostic],
) -> ReadOnlyArray[@diagrams.Diagnostic] {
  ReadOnlyArray::from_array(
    values.map(d => {
      ..d,
      code: d.code.replace_all(old="swimlane/", new="flowchart/"),
      message: d.message
      .replace_all(old="Swimlane", new="Flowchart")
      .replace_all(old="swimlane", new="flowchart"),
    }),
  )
}

///|
pub fn layout(
  view : @model.Flowchart,
  limits~ : Limits = Limits::new(),
) -> Result {
  fn failed(code : String, message : String) -> Result {
    {
      geometry: None,
      diagnostics: [@diagrams.Diagnostic::error(code, message)],
    }
  }
  if limits.nodes <= 0 ||
    limits.edges <= 0 ||
    limits.depth <= 0 ||
    limits.work <= 0 ||
    limits.output_units <= 0 ||
    limits.width <= 0 ||
    limits.height <= 0 {
    return failed(
      "flowchart/layout-limit", "Flowchart layout limits must be positive",
    )
  }
  let findings = @model.validate(
    view,
    nodes_limit=limits.nodes,
    edges_limit=limits.edges,
    depth_limit=limits.depth,
    work_limit=limits.work,
  )
  if !findings.is_empty() {
    return { geometry: None, diagnostics: findings, }
  }
  let groups : Map[String, @model.Group] = Map([])
  let ownership : Map[String, Array[String?]] = Map([])
  let group_members : Map[String, Array[String]] = Map([])
  let source_groups : Map[String, ReadOnlyArray[String]] = Map([])
  let group_children : Map[String, Array[String]] = Map([])
  for group in view.groups {
    groups[group.id] = group
    ownership[group.id] = []
    group_members[group.id] = []
    group_children[group.id] = []
  }
  for group in view.groups {
    if group.parent is Some(parent) {
      group_children[parent].push(group.id)
    }
  }
  fn register(
    path : ReadOnlyArray[String],
    lane : String?,
    member_id : String,
  ) {
    for id in path {
      let found = ownership[id]
      if found.length() < 2 && !found.contains(lane) {
        found.push(lane)
      }
      group_members[id].push(member_id)
    }
  }
  for node in view.nodes {
    register(node.groups, node.lane, node.id)
    source_groups[node.id] = node.groups
  }
  for note in view.notes {
    register(note.groups, note.lane, note.id)
  }
  let crossing : Map[String, Bool] = Map([])
  for group in view.groups {
    if ownership[group.id].length() > 1 {
      crossing[group.id] = true
    }
  }
  fn collapsed(path : ReadOnlyArray[String]) -> String? {
    for id in path {
      if crossing.contains(id) && !groups[id].expanded {
        return Some(id)
      }
    }
    None
  }
  fn path_for(path : ReadOnlyArray[String]) -> Array[String] {
    let closed = collapsed(path)
    let mut entered = false
    let result = []
    for id in path {
      if Some(id) == closed {
        entered = true
      }
      if !crossing.contains(id) || (entered && Some(id) == closed) {
        result.push(id)
      }
    }
    result
  }
  let path_cache : Map[String, Array[String]] = Map([])
  fn group_path(id : String) -> Array[String] {
    if path_cache.get(id) is Some(path) {
      return path
    }
    let result = []
    let mut cursor = groups[id].parent
    while cursor is Some(parent) {
      result.push(parent)
      cursor = groups[parent].parent
    }
    let ordered = []
    for i = result.length() - 1; i >= 0; i = i - 1 {
      ordered.push(result[i])
    }
    path_cache[id] = ordered
    ordered
  }
  fn group_lane(id : String) -> String? {
    let path = group_path(id)
    if collapsed(ReadOnlyArray::from_array(path)) != None ||
      crossing.contains(id) {
      return None
    }
    let mut cursor = Some(id)
    while cursor is Some(current) {
      if groups[current].lane is Some(lane) {
        return Some(lane)
      }
      let found = ownership[current]
      if found.length() == 1 && found[0] is Some(lane) {
        return Some(lane)
      }
      if crossing.contains(current) {
        return None
      }
      cursor = groups[current].parent
    }
    None
  }
  let intentional : Array[(String, String)] = []
  for node in view.nodes {
    for id in node.groups {
      intentional.push((node.id, id))
      if node.lane is Some(lane) {
        intentional.push((lane, id))
      }
    }
  }
  for note in view.notes {
    for id in note.groups {
      intentional.push((note.id, id))
      if note.lane is Some(lane) {
        intentional.push((lane, id))
      }
    }
  }
  for group in view.groups {
    for parent in group_path(group.id) {
      intentional.push((group.id, parent))
    }
  }
  let nodes : Array[@vector.Node] = view.nodes
    .map(n => {
      @vector.Node::new(
        n.id,
        label=n.label,
        label_kind=vector_label(n.label_kind),
        shape=n.shape,
        metadata=n.metadata[:],
        lane?=if collapsed(n.groups) == None { n.lane } else { None },
        groups=path_for(n.groups)[:],
        properties?=n.properties.map(p => {
          @vector.RenderProperties::new(link?=p.link, metadata=p.metadata[:])
        }),
      )
    })
    .iter()
    .to_array()
  let connections : Array[@model.Connection] = view.connections
    .iter()
    .to_array()
  let used : Map[String, Bool] = Map([])
  for n in view.nodes {
    used[n.id] = true
  }
  for g in view.groups {
    used[g.id] = true
  }
  for l in view.lanes {
    used[l.id] = true
  }
  for e in connections {
    used[e.id] = true
  }
  for n in view.notes {
    used[n.id] = true
  }
  let mut work = 0
  for i, note in view.notes {
    nodes.push(
      @vector.Node::new(
        note.id,
        label=note.text,
        shape="note",
        lane?=if collapsed(note.groups) == None { note.lane } else { None },
        groups=path_for(note.groups)[:],
      ),
    )
    for j, target in note.targets {
      if connections.length() >= limits.edges {
        return failed(
          "flowchart/layout-limit", "Note attachments exceed the connection limit",
        )
      }
      let stem = "~note:" + i.to_string() + ":" + j.to_string()
      let mut id = stem
      let mut suffix = 0
      while used.contains(id) {
        if work >= limits.work {
          return failed(
            "flowchart/layout-limit", "Note identity work limit exceeded",
          )
        }
        work += 1
        suffix += 1
        id = stem + ":" + suffix.to_string()
      }
      used[id] = true
      connections.push(
        @model.Connection::new(
          id,
          target,
          note.id,
          style=Dotted,
          target_marker=None,
          role=NoteAttachment,
        ),
      )
    }
  }
  let vector_groups = view.groups
    .map(g => {
      @vector.Group::new(
        g.id,
        label=g.label,
        parent?=path_for(ReadOnlyArray::from_array(group_path(g.id))).last(),
        lane?=group_lane(g.id),
        direction?=g.direction.map(vector_direction),
        expanded=g.expanded,
      )
    })
    .iter()
    .to_array()
  let group_indices : Map[String, Int32] = Map([])
  for i, group in vector_groups {
    group_indices[group.id] = i
  }
  let node_indices : Map[String, Int32] = Map([])
  let original_paths : Map[String, ReadOnlyArray[String]] = Map([])
  for i, node in nodes {
    node_indices[node.id] = i
    original_paths[node.id] = node.groups
  }
  let wrappers : Map[String, String] = Map([])
  let wrapper_depth : Map[String, Int32] = Map([])
  let wrapped : Map[String, Array[Int32]] = Map([])
  let hidden_containers = []
  fn replace_path(index : Int32, path : ArrayView[String]) {
    let node = nodes[index]
    nodes[index] = @vector.Node::new(
      node.id,
      label=node.label,
      label_kind=node.label_kind,
      shape=node.shape,
      metadata=node.metadata[:],
      lane?=node.lane,
      groups=path,
      properties?=node.properties,
    )
  }
  for i, note in view.notes {
    let anchor = note.targets
      .get(0)
      .map(x => Some(x))
      .unwrap_or_else(() => {
        note.connection_targets
        .get(0)
        .bind(id => view.connections.iter().find_first(c => c.id == id))
        .map(c => c.source)
      })
    if note.placement != "floating" &&
      anchor is Some(anchor) &&
      node_indices.get(anchor) is Some(anchor_index) &&
      node_indices.get(note.id) is Some(note_index) &&
      original_paths[anchor] == original_paths[note.id] &&
      nodes[anchor_index].lane == nodes[note_index].lane {
      let depth = wrapper_depth.get(anchor).unwrap_or(0) + 1
      if depth > limits.depth - original_paths[anchor].length() {
        return failed(
          "flowchart/layout-limit", "Note placement exceeds the nesting limit",
        )
      }
      wrapper_depth[anchor] = depth
      let stem = "~note-layout:" + i.to_string()
      let mut id = stem
      let mut suffix = 0
      while used.contains(id) {
        if work >= limits.work {
          return failed(
            "flowchart/layout-limit", "Note container identity limit exceeded",
          )
        }
        work += 1
        suffix += 1
        id = stem + ":" + suffix.to_string()
      }
      if nodes.length() + vector_groups.length() + view.lanes.length() >=
        limits.nodes {
        return failed(
          "flowchart/layout-limit", "Note placement containers exceed the node limit",
        )
      }
      used[id] = true
      hidden_containers.push(id)
      let original = original_paths[anchor]
      let direction : @vector.Direction = match note.placement {
        "left" => RL
        "top" => BT
        "bottom" => TB
        _ => LR
      }
      let group = @vector.Group::new(
        id,
        label="",
        parent?=original.last(),
        lane?=nodes[anchor_index].lane,
        direction~,
      )
      group_indices[id] = vector_groups.length()
      vector_groups.push(group)
      let members = wrapped.get(anchor).unwrap_or([anchor_index])
      if wrappers.get(anchor) is Some(previous) {
        let index = group_indices[previous]
        let old = vector_groups[index]
        vector_groups[index] = @vector.Group::new(
          old.id,
          label=old.label,
          parent=id,
          lane?=old.lane,
          direction?=old.direction,
          expanded=old.expanded,
        )
      }
      for index in members {
        let units = nodes[index].groups.length() + original.length() + 2
        if units > limits.work - work {
          return failed(
            "flowchart/layout-limit", "Note placement work limit exceeded",
          )
        }
        work += units
        let path = original.iter().concat([id].iter()).to_array()
        for j = original.length(); j < nodes[index].groups.length(); j = j + 1 {
          path.push(nodes[index].groups[j])
        }
        replace_path(index, path[:])
      }
      replace_path(
        note_index,
        original.iter().concat([id].iter()).to_array()[:],
      )
      members.push(note_index)
      wrapped[anchor] = members
      wrappers[anchor] = id
      if source_groups.get(anchor) is Some(path) {
        for parent in path {
          intentional.push((id, parent))
        }
      }
    }
  }
  let vector = @vector.Swimlane::new(
    view.id,
    nodes[:],
    connections.map(e => {
      @vector.Connection::new(
        e.id,
        e.source,
        e.target,
        label=e.label,
        label_kind=vector_label(e.label_kind),
        style=vector_style(e.style),
        source_marker=vector_marker(e.source_marker),
        target_marker=vector_marker(e.target_marker),
        min_length=e.min_length,
      )
    })[:],
    direction=vector_direction(view.direction),
    lanes=view.lanes.map(l => {
        @vector.Lane::new(
          l.id,
          label=l.label,
          direction?=l.direction.map(vector_direction),
          expanded=l.expanded,
        )
      })[:],
    groups=vector_groups[:],
    title?=view.title,
    accessibility_title?=view.accessibility_title,
    accessibility_description?=view.accessibility_description,
  )
  let direction_hints = view.connections
    .iter()
    .filter_map(e => e.direction_hint.map(d => (e.id, vector_direction(d))))
    .to_array()
  let geometry_limits = @geometry.Limits::new(
    minimum_approach=limits.minimum_approach,
    nodes=limits.nodes,
    edges=limits.edges,
    depth=limits.depth,
    work=limits.work,
    output_units=limits.output_units,
  )
  let initial = @geometry.layout(
    vector,
    limits=geometry_limits,
    flowchart=true,
    direction_hints~,
  )
  let mut overlay_applied = false
  let framed = match initial.geometry {
    Some(drawing) => {
      let overlays = []
      let ordered = view.groups
        .iter()
        .filter(g => crossing.contains(g.id))
        .to_array()
      ordered.sort_by((a, b) => {
        group_path(b.id).length().compare(group_path(a.id).length())
      })
      for group in ordered {
        let members = group_members[group.id]
          .iter()
          .concat(group_children[group.id].iter())
          .to_array()
        overlays.push(
          @geometry.Overlay::new(group.id, members[:], parent?=group.parent),
        )
      }
      if overlays.is_empty() {
        initial
      } else {
        overlay_applied = true
        drawing.overlay(overlays[:], limits=geometry_limits, direction_hints~)
      }
    }
    None => initial
  }
  let annotations = []
  for i, note in view.notes {
    for j, target in note.connection_targets {
      if connections.length() >= limits.edges {
        return failed(
          "flowchart/layout-limit", "Edge note attachments exceed the connection limit",
        )
      }
      let stem = "~edge-note:" + i.to_string() + ":" + j.to_string()
      let mut id = stem
      let mut suffix = 0
      while used.contains(id) {
        if work >= limits.work {
          return failed(
            "flowchart/layout-limit", "Note identity limit exceeded",
          )
        }
        work += 1
        suffix += 1
        id = stem + ":" + suffix.to_string()
      }
      used[id] = true
      annotations.push(@geometry.ConnectionNote::new(id, target, note.id))
      connections.push(
        @model.Connection::new(
          id,
          target,
          note.id,
          style=Dotted,
          target_marker=None,
          role=NoteAttachment,
        ),
      )
    }
  }
  let prior = if overlay_applied {
    initial.diagnostics.iter().to_array()
  } else {
    []
  }
  if !annotations.is_empty() && framed.geometry is Some(_) {
    prior.append(framed.diagnostics)
  }
  let result = if annotations.is_empty() {
    framed
  } else {
    match framed.geometry {
      Some(drawing) => drawing.annotate(annotations[:], limits=geometry_limits)
      None => framed
    }
  }
  let accumulated = diagnostics(
    prior.iter().concat(result.diagnostics.iter()).to_array()[:],
  )
  guard result.geometry is Some(drawing) else {
    return { geometry: None, diagnostics: accumulated, }
  }
  if drawing.width > limits.width || drawing.height > limits.height {
    return failed(
      "flowchart/layout-limit", "Flowchart dimensions exceed the configured bounds",
    )
  }
  let by_id : Map[String, @model.Connection] = Map([])
  for edge in connections {
    by_id[edge.id] = edge
  }
  let routed = drawing.connections.map(e => Connection::{
    connection: by_id[e.connection.id],
    index: e.index,
    source: e.source,
    target: e.target,
    line: e.line,
    label: e.label,
  })
  {
    geometry: Some({
      view,
      shapes: drawing.shapes,
      connections: routed,
      width: drawing.width,
      height: drawing.height,
      drawing,
      drawing_result: result,
      prior_diagnostics: ReadOnlyArray::from_array(prior),
      intentional: ReadOnlyArray::from_array(intentional),
      hidden_containers: ReadOnlyArray::from_array(hidden_containers),
    }),
    diagnostics: accumulated,
  }
}

///|
/// Shared low-level geometry for the vector painter, separate from the model.
pub fn Geometry::drawing(self : Geometry) -> @geometry.Geometry {
  self.drawing
}

///|
pub fn check(
  geometry : Geometry,
  palette : @theme.Palette,
  work_limit~ : Int32 = 1000000,
) -> ReadOnlyArray[@geometry.Finding] {
  @geometry.check(
    geometry.drawing,
    @geometry.palette(palette),
    work_limit~,
    intentional_containment=geometry.intentional[:],
  )
}

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

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

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

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

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

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

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

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

///|
pub fn Geometry::drawing_result(self : Geometry) -> @geometry.Result {
  self.drawing_result
}

///|
pub fn Geometry::intentional_containment(
  self : Geometry,
) -> ReadOnlyArray[(String, String)] {
  self.intentional
}

///|
pub fn Geometry::hidden_containers(self : Geometry) -> ReadOnlyArray[String] {
  self.hidden_containers
}

///|
/// Findings from geometry phases before the final shared drawing phase.
pub fn Geometry::prior_diagnostics(
  self : Geometry,
) -> ReadOnlyArray[@diagrams.Diagnostic] {
  self.prior_diagnostics
}

///|
/// Retain resource limits while selecting the marked terminal reservation.
pub fn Limits::with_minimum_approach(
  self : Limits,
  minimum_approach : Double,
) -> Limits {
  { ..self, minimum_approach, }
}