///|
pub fn attributes(entity : Entity) -> Array[EntityAttribute] {
  let result : Array[EntityAttribute] = []
  for index in 0.. EntityAttribute? {
  match entity_value(entity, index) {
    Some(value) => Some(EntityAttribute::{ index, value })
    None => None
  }
}

///|
pub fn attribute_text(entity : Entity, index : Int) -> String? {
  match attribute(entity, index) {
    Some(item) => Some(item.value.to_step())
    None => None
  }
}

///|
pub fn entities_referencing(index : ModelIndex, id : Int) -> Array[Entity] {
  index.all().filter(entity => entity_references(entity).contains(id))
}

///|
/// Follow generic STEP references without imposing an IFC schema.
pub fn reachable_entities(
  index : ModelIndex,
  roots : Array[Int],
) -> Array[Entity] {
  let queue = roots.copy()
  let visited : Map[Int, Bool] = Map([])
  let result : Array[Entity] = []
  let mut cursor = 0
  while cursor < queue.length() {
    let id = queue[cursor]
    cursor += 1
    if !visited.contains(id) {
      visited[id] = true
      match index.entity(id) {
        Some(entity) => {
          result.push(entity)
          for target in entity_references(entity) {
            if !visited.contains(target) {
              queue.push(target)
            }
          }
        }
        None => ()
      }
    }
  }
  result
}

///|
pub fn type_histogram(index : ModelIndex) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for entity in index.all() {
    result.update_or_default(entity.name.to_upper(), 1, count => count + 1)
  }
  result
}

///|
pub fn graph_stats(index : ModelIndex) -> GraphStats {
  let targets : Map[Int, Bool] = Map([])
  let mut reference_count = 0
  let mut max_out_degree = 0
  let mut orphan_reference_count = 0
  for entity in index.all() {
    let refs = entity_references(entity)
    reference_count += refs.length()
    if refs.length() > max_out_degree {
      max_out_degree = refs.length()
    }
    for target in refs {
      targets[target] = true
      if !index.has(target) {
        orphan_reference_count += 1
      }
    }
  }
  {
    entity_count: index.all().length(),
    reference_count,
    distinct_targets: targets.length(),
    max_out_degree,
    orphan_reference_count,
  }
}

///|
pub fn dependency_layers(
  index : ModelIndex,
  roots : Array[Int],
) -> Array[Array[Int]] {
  let current = roots
  let visited : Map[Int, Bool] = Map([])
  let layers : Array[Array[Int]] = []
  for root in roots {
    visited[root] = true
  }
  let mut layer = current
  while layer.length() > 0 {
    layers.push(layer)
    let next : Array[Int] = []
    for id in layer {
      match index.entity(id) {
        Some(entity) =>
          for target in entity_references(entity) {
            if !visited.contains(target) {
              visited[target] = true
              next.push(target)
            }
          }
        None => ()
      }
    }
    layer = next
  }
  layers
}