///|
fn contains_folded(text : String, term : String) -> Bool {
  text.to_lower().contains(term.to_lower())
}

///|
pub fn search_entities(index : ModelIndex, term : String) -> Array[SearchHit] {
  let hits : Array[SearchHit] = []
  for entity in index.all() {
    if contains_folded(entity.name, term) {
      hits.push(SearchHit::{
        entity_id: entity.id,
        product_id: None,
        field: EntityType,
        value: entity.name,
      })
    }
    for argument in entity.arguments {
      let text = argument.to_step_lossless()
      if contains_folded(text, term) {
        hits.push(SearchHit::{
          entity_id: entity.id,
          product_id: None,
          field: EntityText,
          value: text,
        })
      }
    }
  }
  hits
}

///|
fn product_search_hit(
  product : Product,
  field : SearchField,
  value : String,
) -> SearchHit {
  SearchHit::{
    entity_id: product.id,
    product_id: Some(product.id),
    field,
    value,
  }
}

///|
pub fn search_products(
  index : ModelIndex,
  field : SearchField,
  term : String,
) -> Array[SearchHit] {
  let hits : Array[SearchHit] = []
  for product in extract_products(index) {
    match field {
      ProductName =>
        match product.name {
          Some(value) if contains_folded(value, term) =>
            hits.push(product_search_hit(product, field, value))
          _ => ()
        }
      ProductGlobalId =>
        match product.global_id {
          Some(value) if contains_folded(value, term) =>
            hits.push(product_search_hit(product, field, value))
          _ => ()
        }
      ProductTag =>
        match product.tag {
          Some(value) if contains_folded(value, term) =>
            hits.push(product_search_hit(product, field, value))
          _ => ()
        }
      EntityType if contains_folded(product.ifc_type, term) =>
        hits.push(product_search_hit(product, field, product.ifc_type))
      _ => ()
    }
  }
  hits
}

///|
pub fn search_properties(
  index : ModelIndex,
  term : String,
) -> Array[SearchHit] raise ParseError {
  let hits : Array[SearchHit] = []
  for property_set in all_property_sets(index) {
    for property in property_set.properties {
      let name_match = contains_folded(property.name, term)
      let value_text = property.value.to_step_lossless()
      let value_match = contains_folded(value_text, term)
      if name_match || value_match {
        hits.push(SearchHit::{
          entity_id: property_set.id,
          product_id: None,
          field: if name_match {
            PropertyName
          } else {
            PropertyValue
          },
          value: if name_match {
            property.name
          } else {
            value_text
          },
        })
      }
    }
  }
  hits
}

///|
pub fn search(
  model : Model,
  field : SearchField,
  term : String,
) -> Array[SearchHit] raise ParseError {
  let index = model.index()
  match field {
    EntityType | EntityText => search_entities(index, term)
    ProductName | ProductGlobalId | ProductTag =>
      search_products(index, field, term)
    PropertyName | PropertyValue => search_properties(index, term)
  }
}

///|
pub fn search_first_product(
  index : ModelIndex,
  field : SearchField,
  term : String,
) -> Product? {
  let hits = search_products(index, field, term)
  match hits.get(0) {
    Some(hit) =>
      match hit.product_id {
        Some(id) => product_by_id(extract_products(index), id)
        None => None
      }
    None => None
  }
}