///|
pub fn build_graph_bytes(bytes : Bytes) -> DependencyGraph raise @xlsx.XlsxError {
  let workbook = @xlsx.read(bytes)
  let graph = DependencyGraph::new()
  for name in workbook.get_sheet_list() {
    let sheet = workbook.sheet(name).unwrap()
    let max_row = sheet.max_row()
    let max_col = sheet.max_col()
    for row in 1..=max_row {
      for col in 1..=max_col {
        let cell = @xlsx.coordinates_to_cell_name(col, row)
        match sheet.get_cell_formula(cell) {
          Some(text) => {
            let key = node_key(name, row, col)
            ignore(graph.edges.get_or_init(key, fn() { [] }))
            for r in extract_refs(text, name).cells {
              let target_sheet = match r.sheet {
                Some(value) => value
                None => name
              }
              let row_lo = if r.row < r.end_row { r.row } else { r.end_row }
              let row_hi = if r.row < r.end_row { r.end_row } else { r.row }
              let col_lo = if r.col < r.end_col { r.col } else { r.end_col }
              let col_hi = if r.col < r.end_col { r.end_col } else { r.col }
              let mut added = 0
              for rr in row_lo..=row_hi {
                for cc in col_lo..=col_hi {
                  if added < 500 {
                    add_edge(graph, key, node_key(target_sheet, rr, cc))
                    added = added + 1
                  }
                }
              }
            }
          }
          None => ()
        }
      }
    }
  }
  graph
}

///|
pub fn trace_bytes(
  bytes : Bytes,
  start : String,
  direction : String,
  max_depth : Int,
) -> Array[String] raise @xlsx.XlsxError {
  let graph = build_graph_bytes(bytes)
  let result : Array[String] = [start]
  let visited : Map[String, Bool] = Map([(start, true)])
  let mut frontier : Array[String] = [start]
  let mut depth = 0
  while depth < max_depth && frontier.length() > 0 {
    let next_frontier : Array[String] = []
    for node in frontier {
      let neighbors = if direction == "dependents" {
        graph.reverse.get(node)
      } else {
        graph.edges.get(node)
      }
      match neighbors {
        Some(list) =>
          for target in list {
            if !visited.contains(target) {
              visited.set(target, true)
              result.push(target)
              next_frontier.push(target)
            }
          }
        None => ()
      }
    }
    frontier = next_frontier
    depth = depth + 1
  }
  result
}