///|
pub struct SearchHit {
  code : String
  title : String
  score : Int
} derive(Debug, Eq, ToJson)

///|
pub fn Catalog::ranked_search(
  self : Catalog,
  query : String,
  limit : Int,
) -> Array[SearchHit] {
  if limit <= 0 {
    return []
  }
  let normalized_query = query.trim().to_owned().to_lower()
  if normalized_query.is_empty() {
    return []
  }
  let tokens = search_tokens(normalized_query)
  let ranked = []
  for entry in self.entries {
    let score = score_entry(entry, normalized_query, tokens)
    if score > 0 {
      insert_search_hit(ranked, { code: entry.code, title: entry.title, score })
    }
  }
  if ranked.length() > limit {
    ranked[:limit].to_owned()
  } else {
    ranked
  }
}

///|
fn search_tokens(query : String) -> Array[String] {
  let tokens = []
  let mut current = ""
  for c in query {
    if c == ' ' || c == '\t' || c == '\n' {
      if !current.is_empty() {
        tokens.push(current)
        current = ""
      }
    } else {
      current = current + c.to_string()
    }
  }
  if !current.is_empty() {
    tokens.push(current)
  }
  tokens
}

///|
fn score_entry(entry : IcdEntry, query : String, tokens : Array[String]) -> Int {
  let title = entry.title.to_lower()
  let code = entry.code.to_lower()
  let mut score = 0
  if title.contains(query) {
    score += 10
  }
  if code == query {
    score += 20
  } else if code.has_prefix(query) {
    score += 6
  }
  for token in tokens {
    if title.contains(token) {
      score += 3
    }
    if code.contains(token) {
      score += 1
    }
  }
  score
}

///|
fn insert_search_hit(ranked : Array[SearchHit], hit : SearchHit) -> Unit {
  let next = []
  let mut inserted = false
  for current in ranked {
    if !inserted &&
      (
        hit.score > current.score ||
        (hit.score == current.score && hit.code < current.code)
      ) {
      next.push(hit)
      inserted = true
    }
    next.push(current)
  }
  if !inserted {
    next.push(hit)
  }
  ranked.clear()
  for current in next {
    ranked.push(current)
  }
}