///|
fn adjacency_add_space(
  map : Map[Int, Array[Int]],
  element_id : Int,
  space_id : Int,
) -> Unit {
  map.update_or_default(element_id, [space_id], values => {
    if !values.contains(space_id) {
      values.push(space_id)
    }
    values
  })
}

///|
fn adjacency_boundary_map(index : ModelIndex) -> Map[Int, Array[Int]] {
  let result : Map[Int, Array[Int]] = Map([])
  for entity in index.all() {
    if entity.name.to_upper() == "IFCRELSPACEBOUNDARY" {
      match
        (
          optional_reference_argument(entity, 4),
          optional_reference_argument(entity, 5),
        ) {
        (Some(element), Some(space)) =>
          adjacency_add_space(result, element, space)
        _ => ()
      }
    }
  }
  result
}

///|
fn adjacency_edge_key(left : Int, right : Int) -> String {
  if left < right {
    left.to_string() + ":" + right.to_string()
  } else {
    right.to_string() + ":" + left.to_string()
  }
}

///|
fn adjacency_add_neighbor(
  map : Map[Int, Array[Int]],
  left : Int,
  right : Int,
) -> Unit {
  map.update_or_default(left, [right], values => {
    if !values.contains(right) {
      values.push(right)
    }
    values
  })
}

///|

///|
/// Build a room adjacency graph from IfcRelSpaceBoundary records.
pub fn SpaceAdjacencyGraph::new(
  model : Model,
) -> SpaceAdjacencyGraph raise ParseError {
  let index = model.index()
  let spatial = SpatialIndex::new(model)
  let boundaries = adjacency_boundary_map(index)
  let spaces = index.ids_named("IFCSPACE")
  let edges : Array[SpaceAdjacency] = []
  let neighbors : Map[Int, Array[Int]] = Map([])
  let seen : Map[String, Bool] = Map([])
  for _, space_ids in boundaries {
    for i in 0.. box_distance(a.bounds, b.bounds)
          _ => 0.0
        }
        edges.push({
          left_space: if left < right {
            left
          } else {
            right
          },
          right_space: if left < right {
            right
          } else {
            left
          },
          shared_elements: shared,
          boundary_count: shared.length(),
          distance,
        })
        adjacency_add_neighbor(neighbors, left, right)
        adjacency_add_neighbor(neighbors, right, left)
      }
    }
  }
  { spaces, edges, neighbors }
}

///|
pub fn space_adjacency(model : Model) -> SpaceAdjacencyGraph raise ParseError {
  SpaceAdjacencyGraph::new(model)
}

///|
pub fn SpaceAdjacencyGraph::edge_count(self : SpaceAdjacencyGraph) -> Int {
  self.edges.length()
}

///|
pub fn SpaceAdjacencyGraph::space_count(self : SpaceAdjacencyGraph) -> Int {
  self.spaces.length()
}

///|
pub fn SpaceAdjacencyGraph::neighbors(
  self : SpaceAdjacencyGraph,
  id : Int,
) -> Array[Int] {
  self.neighbors.get(id).unwrap_or([])
}

///|
pub fn SpaceAdjacencyGraph::degree(self : SpaceAdjacencyGraph, id : Int) -> Int {
  self.neighbors(id).length()
}

///|
pub fn SpaceAdjacencyGraph::edge(
  self : SpaceAdjacencyGraph,
  left : Int,
  right : Int,
) -> SpaceAdjacency? {
  for edge in self.edges {
    if (edge.left_space == left && edge.right_space == right) ||
      (edge.left_space == right && edge.right_space == left) {
      return Some(edge)
    }
  }
  None
}

///|
pub fn SpaceAdjacencyGraph::isolated_spaces(
  self : SpaceAdjacencyGraph,
) -> Array[Int] {
  self.spaces.filter(id => self.degree(id) == 0)
}

///|
pub fn SpaceAdjacencyGraph::reachable(
  self : SpaceAdjacencyGraph,
  start : Int,
) -> Array[Int] {
  let result : Array[Int] = []
  let queue : Array[Int] = [start]
  while queue.length() > 0 {
    let current = queue.remove(0)
    if result.contains(current) {
      continue
    }
    result.push(current)
    for next in self.neighbors(current) {
      if !result.contains(next) {
        queue.push(next)
      }
    }
  }
  result
}

///|
pub fn SpaceAdjacencyGraph::shortest_path(
  self : SpaceAdjacencyGraph,
  start : Int,
  goal : Int,
) -> Array[Int]? {
  if start == goal {
    return Some([start])
  }
  let queue : Array[Int] = [start]
  let visited : Map[Int, Bool] = Map([])
  let parents : Map[Int, Int] = Map([])
  visited[start] = true
  while queue.length() > 0 {
    let current = queue.remove(0)
    for next in self.neighbors(current) {
      if visited.contains(next) {
        continue
      }
      visited[next] = true
      parents[next] = current
      if next == goal {
        queue.clear()
        break
      }
      queue.push(next)
    }
  }
  if !visited.contains(goal) {
    return None
  }
  let reversed : Array[Int] = [goal]
  let mut cursor = goal
  while cursor != start {
    match parents.get(cursor) {
      Some(parent) => {
        cursor = parent
        reversed.push(cursor)
      }
      None => break
    }
  }
  let result : Array[Int] = []
  for index in 0.. String {
  "#" +
  self.left_space.to_string() +
  " <-> #" +
  self.right_space.to_string() +
  " shared=" +
  self.shared_elements.map(id => "#" + id.to_string()).join(",") +
  " distance=" +
  self.distance.to_string()
}

///|
pub fn SpaceAdjacencyGraph::headline(self : SpaceAdjacencyGraph) -> String {
  "spaces=" +
  self.spaces.length().to_string() +
  " edges=" +
  self.edges.length().to_string() +
  " isolated=" +
  self.isolated_spaces().length().to_string()
}

///|
pub fn SpaceAdjacencyGraph::to_json(self : SpaceAdjacencyGraph) -> String {
  let edges = self.edges
    .map(edge => {
      "{\"left\":" +
      edge.left_space.to_string() +
      ",\"right\":" +
      edge.right_space.to_string() +
      ",\"shared_elements\":[" +
      edge.shared_elements.map(id => id.to_string()).join(",") +
      "],\"distance\":" +
      edge.distance.to_string() +
      "}"
    })
    .join(",")
  "{\"space_count\":" +
  self.spaces.length().to_string() +
  ",\"edge_count\":" +
  self.edges.length().to_string() +
  ",\"isolated\":" +
  self.isolated_spaces().length().to_string() +
  ",\"edges\":[" +
  edges +
  "]}"
}

///|
pub fn SpaceAdjacencyGraph::to_markdown(self : SpaceAdjacencyGraph) -> String {
  let builder = StringBuilder()
  builder <+ "# Space adjacency\n\n"
  builder <+ "- Spaces: \{self.spaces.length()}\n"
  builder <+ "- Edges: \{self.edges.length()}\n"
  builder <+ "- Isolated: \{self.isolated_spaces().length()}\n\n"
  for edge in self.edges {
    builder <+ "- \{edge.headline()}\n"
  }
  builder.to_string()
}

///|
pub fn space_neighbors(
  model : Model,
  space_id : Int,
) -> Array[Int] raise ParseError {
  space_adjacency(model).neighbors(space_id)
}

///|
pub fn space_path(
  model : Model,
  start : Int,
  goal : Int,
) -> Array[Int]? raise ParseError {
  space_adjacency(model).shortest_path(start, goal)
}