///|
/// Build a validated exact Flat index from a complete corpus.
pub fn build_flat_index(docs : Array[Document]) -> FlatIndex raise VectorError {
  if docs.length() > 0 {
    validate_documents(docs)
  }
  let index = FlatIndex::new()
  for doc in docs {
    index.add(doc)
  }
  index
}

///|
/// Build a validated IVF-Flat index.
pub fn build_ivf_index(
  docs : Array[Document],
  k : Int,
  metric : DistanceMetric,
) -> IvfIndex raise VectorError {
  validate_documents(docs)
  if k <= 0 {
    raise InvalidK
  }
  if docs.length() < k {
    raise ClusterError("Cluster count exceeds corpus size")
  }
  let index = IvfIndex::new(k, metric)
  index.build(docs)
  index
}

///|
/// Build a KD-Tree while checking corpus dimensions first.
pub fn build_kd_tree_index(
  docs : Array[Document],
  metric : DistanceMetric,
) -> KdTreeIndex raise VectorError {
  validate_documents(docs)
  let index = KdTreeIndex::new(metric)
  index.build(docs)
  index
}

///|
/// Build an LSH index with deterministic planes.
pub fn build_lsh_index(
  docs : Array[Document],
  num_planes : Int,
) -> LshIndex raise VectorError {
  validate_documents(docs)
  if num_planes <= 0 {
    raise InvalidK
  }
  let index = LshIndex::new(num_planes, docs[0].vector.length())
  for doc in docs {
    index.add(doc)
  }
  index
}

///|
/// Apply a single metadata annotation without mutating input documents.
pub fn set_metadata(doc : Document, key : String, value : String) -> Document {
  let metadata = []
  let mut replaced = false
  for pair in doc.metadata {
    if pair.0 == key {
      if !replaced {
        metadata.push((key, value))
        replaced = true
      }
    } else {
      metadata.push(pair)
    }
  }
  if !replaced {
    metadata.push((key, value))
  }
  Document::new(doc.id, doc.vector, metadata)
}

///|
/// Remove all documents that match every filter pair.
pub fn remove_matching_documents(
  docs : Array[Document],
  filters : Array[(String, String)],
) -> Array[Document] {
  let remaining = []
  for doc in docs {
    if !matches_filters(doc.metadata, filters) {
      remaining.push(doc)
    }
  }
  remaining
}

///|
/// Copy documents while selecting a stable subset of ids.
pub fn select_document_ids(
  docs : Array[Document],
  ids : Array[String],
) -> Array[Document] {
  let wanted = Map([])
  for id in ids {
    wanted.set(id, true)
  }
  let selected = []
  for doc in docs {
    if wanted.contains(doc.id) {
      selected.push(doc)
    }
  }
  selected
}

///|
/// Return a vector's L2 norm without changing it.
pub fn vector_norm(vector : Array[Double]) -> Double raise VectorError {
  if vector.length() == 0 {
    raise EmptyVector
  }
  let mut squared = 0.0
  for value in vector {
    squared = squared + value * value
  }
  squared.sqrt()
}