// Shared symbol tables and the catalog that holds them.
//
// A local symbol table can import shared symbol tables by name and version.
// A reader finds them in a catalog, which the caller supplies: shared tables
// live outside the stream, for example in a file of
// `$ion_shared_symbol_table::{...}` structs.

///|
/// A shared symbol table: a name, a version, and its symbols in ID order.
/// `None` is a slot with unknown text.
struct SharedSymbolTable {
  name : String
  version : Int32
  symbols : Array[String?]
}

///|
/// A shared symbol table named `name`, at `version`, declaring `symbols`.
pub fn SharedSymbolTable::new(
  name : String,
  version : Int32,
  symbols : Array[String?],
) -> SharedSymbolTable {
  { name, version, symbols, }
}

///|
/// The table's name.
pub fn SharedSymbolTable::name(self : SharedSymbolTable) -> String {
  self.name
}

///|
/// The table's version.
pub fn SharedSymbolTable::version(self : SharedSymbolTable) -> Int32 {
  self.version
}

///|
/// The table's symbols in ID order; `None` is a slot with unknown text.
pub fn SharedSymbolTable::symbols(self : SharedSymbolTable) -> Array[String?] {
  self.symbols
}

///|
/// The shared table a value declares: a struct annotated
/// `$ion_shared_symbol_table` with a non-empty string `name`, an optional
/// positive `version` (1 when missing), and a `symbols` list, where an element
/// that is not a string is a slot with unknown text. `None` means the value is
/// not a shared table. The `imports` field is informational metadata and is
/// ignored; only `symbols` declares slots.
pub fn SharedSymbolTable::from_value(
  value : IonValue,
) -> SharedSymbolTable? raise IonError {
  let annotated = match value.annotations() {
    [first, ..] => first.text() == Some("$ion_shared_symbol_table")
    [] => false
  }
  let fields = match value.kind() {
    Struct(fields) if annotated => fields
    _ => return None
  }
  let mut name : String? = None
  let mut version = 1
  let symbols : Array[String?] = []
  for field in fields {
    match (field.name().text(), field.value().kind()) {
      (Some("name"), String(text)) if text != "" => name = Some(text)
      (Some("version"), Int(n)) if n >= 1N && n <= 0x7FFF_FFFFN =>
        version = n.to_int()
      (Some("symbols"), List(items)) =>
        for item in items {
          symbols.push(
            match item.kind() {
              String(text) => Some(text)
              _ => None
            },
          )
        }
      _ => ()
    }
  }
  match name {
    Some(name) => Some({ name, version, symbols, })
    None => raise DataModel("a shared symbol table must have a name")
  }
}

///|
/// A catalog of shared symbol tables, by name and version.
struct Catalog {
  tables : Map[String, Array[SharedSymbolTable]]
}

///|
/// An empty catalog.
pub fn Catalog::new() -> Catalog {
  { tables: Map([]), }
}

///|
/// A catalog of the shared tables among `values`; other values are ignored.
pub fn Catalog::load(values : Array[IonValue]) -> Catalog raise IonError {
  let catalog = Catalog::new()
  for value in values {
    match SharedSymbolTable::from_value(value) {
      Some(table) => catalog.add(table)
      None => ()
    }
  }
  catalog
}

///|
/// Add a shared table, replacing one with the same name and version.
pub fn Catalog::add(self : Catalog, table : SharedSymbolTable) -> Unit {
  let versions = self.tables.get(table.name).unwrap_or([])
  let kept = versions.filter(other => other.version != table.version)
  kept.push(table)
  self.tables[table.name] = kept
}

///|
/// The shared table with exactly this name and version.
pub fn Catalog::find(
  self : Catalog,
  name : String,
  version : Int32,
) -> SharedSymbolTable? {
  self.tables
  .get(name)
  .unwrap_or([])
  .iter()
  .find_first(table => table.version == version)
}

///|
/// The shared table with this name and the highest version.
pub fn Catalog::latest(self : Catalog, name : String) -> SharedSymbolTable? {
  let mut best : SharedSymbolTable? = None
  for table in self.tables.get(name).unwrap_or([]) {
    match best {
      Some(current) if current.version >= table.version => ()
      _ => best = Some(table)
    }
  }
  best
}

///|
/// Whether the catalog holds no tables.
pub fn Catalog::is_empty(self : Catalog) -> Bool {
  self.tables.is_empty()
}