///|
pub(all) struct ModelIndex {
  entities : Array[Entity]
  by_id : Map[Int, Entity]
  by_name : Map[String, Array[Int]]
} derive(Debug)

///|
pub fn Model::index(self : Model) -> ModelIndex {
  let by_id : Map[Int, Entity] = Map([])
  let by_name : Map[String, Array[Int]] = Map([])
  for entity in self.entities {
    by_id[entity.id] = entity
    let key = entity.name.to_upper()
    match by_name.get(key) {
      Some(ids) => ids.push(entity.id)
      None => by_name[key] = [entity.id]
    }
  }
  { entities: self.entities, by_id, by_name }
}

///|
pub fn ModelIndex::entity(self : ModelIndex, id : Int) -> Entity? {
  self.by_id.get(id)
}

///|
pub fn ModelIndex::require(
  self : ModelIndex,
  id : Int,
) -> Entity raise ParseError {
  match self.by_id.get(id) {
    Some(entity) => entity
    None =>
      raise ParseError::InvalidReference(
        position=SourcePosition::{ offset: 0, line: 0, column: 0 },
        text="#" + id.to_string(),
      )
  }
}

///|
pub fn ModelIndex::ids_named(self : ModelIndex, name : String) -> Array[Int] {
  self.by_name.get(name.to_upper()).unwrap_or([])
}

///|
pub fn ModelIndex::entities_named(
  self : ModelIndex,
  name : String,
) -> Array[Entity] {
  self
  .ids_named(name)
  .fold(init=Array::new(), (result, id) => {
    match self.by_id.get(id) {
      Some(entity) => {
        result.push(entity)
        result
      }
      None => result
    }
  })
}

///|
pub fn ModelIndex::all(self : ModelIndex) -> Array[Entity] {
  self.entities
}

///|
pub fn ModelIndex::has(self : ModelIndex, id : Int) -> Bool {
  self.by_id.contains(id)
}

///|
pub fn entity_value(entity : Entity, index : Int) -> Value? {
  entity.arguments.get(index)
}

///|
pub fn value_reference(value : Value?) -> Int? {
  match value {
    Some(item) => item.as_reference()
    None => None
  }
}

///|
pub fn references(value : Value) -> Array[Int] {
  match value {
    Reference(id) => [id]
    List(values) =>
      values.fold(init=Array::new(), (result, item) => {
        for id in references(item) {
          result.push(id)
        }
        result
      })
    Typed(name~, value~) => {
      ignore(name)
      references(value)
    }
    _ => []
  }
}

///|
pub fn entity_references(entity : Entity) -> Array[Int] {
  entity.arguments.fold(init=Array::new(), (result, value) => {
    for id in references(value) {
      result.push(id)
    }
    result
  })
}