///|
/// A named collection of dictionaries for applications with multiple domains.
pub struct DictionaryCatalog {
  dictionaries : Map[String, Lexicon]
} derive(Debug)

///|
/// A match annotated with the dictionary that produced it.
pub(all) struct CatalogHit {
  source : String
  hit : LexiconMatch
} derive(Debug, Eq, ToJson)

///|
/// Summary counters for a dictionary catalog.
pub(all) struct CatalogStats {
  dictionary_count : Int
  entry_count : Int
  node_count : Int
  match_count : Int
  distinct_sources : Int
} derive(Debug, Eq, ToJson)

///|
pub fn DictionaryCatalog::new() -> DictionaryCatalog {
  { dictionaries: Map([]) }
}

///|
/// Register or replace a named compiled dictionary.
pub fn DictionaryCatalog::register(
  self : DictionaryCatalog,
  name : String,
  lexicon : Lexicon,
) -> Unit {
  if name.length() > 0 {
    self.dictionaries[name] = lexicon
  }
}

///|
/// Compile and register entries under one name.
pub fn DictionaryCatalog::register_entries(
  self : DictionaryCatalog,
  name : String,
  entries : ArrayView[Entry],
) -> Unit {
  self.register(name, compile(entries))
}

///|
/// Remove a dictionary and report whether it existed.
pub fn DictionaryCatalog::remove(
  self : DictionaryCatalog,
  name : String,
) -> Bool {
  let existed = self.dictionaries.contains(name)
  self.dictionaries.remove(name)
  existed
}

///|
/// Return registered names in lexical order.
pub fn DictionaryCatalog::names(self : DictionaryCatalog) -> Array[String] {
  let output : Array[String] = []
  for name in self.dictionaries.keys() {
    output.push(name)
  }
  output.sort_by((left, right) => left.compare(right))
  output
}

///|
pub fn DictionaryCatalog::dictionary_count(self : DictionaryCatalog) -> Int {
  self.dictionaries.length()
}

///|
/// Look up an exact keyword in one named dictionary.
pub fn DictionaryCatalog::lookup(
  self : DictionaryCatalog,
  name : String,
  keyword : String,
) -> Array[Entry] {
  match self.dictionaries.get(name) {
    Some(lexicon) => lexicon.lookup(keyword)
    None => []
  }
}

///|
/// Return whether any named dictionary contains a keyword.
pub fn DictionaryCatalog::contains(
  self : DictionaryCatalog,
  keyword : String,
) -> Bool {
  for _, lexicon in self.dictionaries {
    if lexicon.contains(keyword) {
      return true
    }
  }
  false
}

///|
/// Scan all named dictionaries and retain source ownership.
pub fn DictionaryCatalog::scan(
  self : DictionaryCatalog,
  text : String,
) -> Array[CatalogHit] {
  let output : Array[CatalogHit] = []
  for name, lexicon in self.dictionaries {
    for hit in compile_automaton(lexicon.entries()).find_all(text) {
      output.push({ source: name, hit })
    }
  }
  sort_catalog_hits(output)
}

///|
/// Scan the catalog with one shared query policy.
pub fn DictionaryCatalog::scan_query(
  self : DictionaryCatalog,
  text : String,
  query : MatchQuery,
) -> Array[CatalogHit] {
  let output : Array[CatalogHit] = []
  for name, lexicon in self.dictionaries {
    for hit in query_lexicon(lexicon, text, query) {
      output.push({ source: name, hit })
    }
  }
  sort_catalog_hits(output)
}

///|
/// Return aggregate catalog counters for a text.
pub fn DictionaryCatalog::stats(
  self : DictionaryCatalog,
  text : String,
) -> CatalogStats {
  let hits = self.scan(text)
  let mut entries = 0
  let mut nodes = 0
  for _, lexicon in self.dictionaries {
    entries += lexicon.entry_count()
    nodes += lexicon.node_count()
  }
  let sources : Map[String, Bool] = Map([])
  for item in hits {
    sources[item.source] = true
  }
  {
    dictionary_count: self.dictionary_count(),
    entry_count: entries,
    node_count: nodes,
    match_count: hits.length(),
    distinct_sources: sources.length(),
  }
}

///|
/// Return all entries in source-name order.
pub fn DictionaryCatalog::entries(
  self : DictionaryCatalog,
) -> Array[(String, Entry)] {
  let output : Array[(String, Entry)] = []
  for name in self.names() {
    match self.dictionaries.get(name) {
      Some(lexicon) =>
        for item in lexicon.entries() {
          output.push((name, item))
        }
      None => ()
    }
  }
  output
}

///|
/// Merge another catalog into this catalog, replacing equal names.
pub fn DictionaryCatalog::merge(
  self : DictionaryCatalog,
  other : DictionaryCatalog,
) -> Unit {
  for name, lexicon in other.dictionaries {
    self.register(name, lexicon)
  }
}

///|
/// Export catalog entries with a source column.
pub fn DictionaryCatalog::to_tsv(self : DictionaryCatalog) -> String {
  let output = StringBuilder()
  output.write_string("source\tkeyword\tlabel\tweight\n")
  for pair in self.entries() {
    let source = pair.0
    let item = pair.1
    output.write_string(
      "\{escape_dictionary_field(source)}\t\{escape_dictionary_field(item.keyword)}\t\{escape_dictionary_field(item.label)}\t\{item.weight}\n",
    )
  }
  output.to_string()
}

///|
/// Return hits from one source only.
pub fn catalog_hits_for_source(
  hits : ArrayView[CatalogHit],
  source : String,
) -> Array[CatalogHit] {
  [
    for item in hits if item.source == source => item
  ]
}

///|
/// Count hits by source name.
pub fn catalog_count_by_source(
  hits : ArrayView[CatalogHit],
) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for item in hits {
    counts[item.source] = counts.get_or_default(item.source, 0) + 1
  }
  counts
}

///|
/// Flatten catalog hits into normal match values.
pub fn catalog_matches(hits : ArrayView[CatalogHit]) -> Array[LexiconMatch] {
  [
    for item in hits => item.hit
  ]
}

///|
/// Return a stable catalog hit signature.
pub fn catalog_hit_signature(hit : CatalogHit) -> String {
  "\{hit.source}:\{match_signature(hit.hit)}"
}

///|
fn sort_catalog_hits(hits : Array[CatalogHit]) -> Array[CatalogHit] {
  hits.sort_by(fn(left, right) {
    if left.hit.start != right.hit.start {
      left.hit.start - right.hit.start
    } else if left.hit.end != right.hit.end {
      right.hit.end - right.hit.start - (left.hit.end - left.hit.start)
    } else {
      left.source.compare(right.source)
    }
  })
  hits
}