///|
/// Domain catalogs for application-level variable schemas.
///
/// A catalog gives names, domains, tags, and simple filtering operations to
/// data-driven model builders. It preserves the finite-domain core while
/// allowing CLI and configuration layers to inspect variables without
/// depending on solver internals.
pub struct DomainEntry {
  id : Int
  name : String
  domain : Domain
  tag : String
}

///|
/// Create a domain entry.
pub fn domain_entry(
  id : Int,
  name : String,
  value_domain : Domain,
  tag : String,
) -> DomainEntry {
  { id, name, domain: value_domain, tag }
}

///|
/// A named domain catalog.
pub struct DomainCatalog {
  entries : Array[DomainEntry]
}

///|
/// Create an empty catalog.
pub fn domain_catalog() -> DomainCatalog {
  { entries: [] }
}

///|
/// Add a unique entry.
pub fn DomainCatalog::add(
  self : DomainCatalog,
  name : String,
  value_domain : Domain,
  tag : String,
) -> Int? {
  for entry in self.entries {
    if entry.name == name {
      return None
    }
  }
  let id = self.entries.length()
  self.entries.push(domain_entry(id, name, value_domain.clone(), tag))
  Some(id)
}

///|
/// Return catalog length.
pub fn DomainCatalog::length(self : DomainCatalog) -> Int {
  self.entries.length()
}

///|
/// Read an entry.
pub fn DomainCatalog::entry(self : DomainCatalog, id : Int) -> DomainEntry {
  if id < 0 || id >= self.entries.length() {
    abort("domain catalog entry is outside the catalog")
  }
  self.entries[id]
}

///|
/// Find an entry by name.
pub fn DomainCatalog::find(self : DomainCatalog, name : String) -> DomainEntry? {
  for entry in self.entries {
    if entry.name == name {
      return Some(entry)
    }
  }
  None
}

///|
/// Return copied entries.
pub fn DomainCatalog::entries(self : DomainCatalog) -> Array[DomainEntry] {
  self.entries.copy()
}

///|
/// Return entries with a tag.
pub fn DomainCatalog::with_tag(
  self : DomainCatalog,
  tag : String,
) -> Array[Int] {
  let result : Array[Int] = []
  for entry in self.entries {
    if entry.tag == tag {
      result.push(entry.id)
    }
  }
  result
}

///|
/// Return all names.
pub fn DomainCatalog::names(self : DomainCatalog) -> Array[String] {
  self.entries.map(entry => entry.name)
}

///|
/// Return all domain sizes.
pub fn DomainCatalog::sizes(self : DomainCatalog) -> Array[Int] {
  self.entries.map(entry => entry.domain.size())
}

///|
/// Return the total Cartesian size capped at a limit.
pub fn DomainCatalog::cartesian_size(self : DomainCatalog, limit : Int) -> Int {
  let cap = if limit < 1 { 1 } else { limit }
  let mut result = 1
  for entry in self.entries {
    result *= entry.domain.size()
    if result >= cap {
      return cap
    }
  }
  result
}

///|
/// Remove a value from one catalog entry.
pub fn DomainCatalog::remove(
  self : DomainCatalog,
  id : Int,
  value : Int,
) -> Bool {
  if id < 0 || id >= self.entries.length() {
    return false
  }
  self.entries[id].domain.remove(value)
}

///|
/// Restrict one domain to an interval.
pub fn DomainCatalog::intersect_range(
  self : DomainCatalog,
  id : Int,
  lower : Int,
  upper : Int,
) -> Bool {
  if id < 0 || id >= self.entries.length() {
    return false
  }
  self.entries[id].domain.intersect_range(lower, upper)
}

///|
/// Restrict one domain to an explicit set.
pub fn DomainCatalog::intersect_values(
  self : DomainCatalog,
  id : Int,
  values : Array[Int],
) -> Bool {
  if id < 0 || id >= self.entries.length() {
    return false
  }
  self.entries[id].domain.intersect_values(values)
}

///|
/// Return empty-domain entries.
pub fn DomainCatalog::empty_entries(self : DomainCatalog) -> Array[Int] {
  let result : Array[Int] = []
  for entry in self.entries {
    if entry.domain.is_empty() {
      result.push(entry.id)
    }
  }
  result
}

///|
/// Return singleton entries.
pub fn DomainCatalog::singleton_entries(
  self : DomainCatalog,
) -> Array[(Int, Int)] {
  let result : Array[(Int, Int)] = []
  for entry in self.entries {
    match entry.domain.singleton() {
      Some(value) => result.push((entry.id, value))
      None => ()
    }
  }
  result
}

///|
/// Return a catalog of cloned domains.
pub fn DomainCatalog::clone(self : DomainCatalog) -> DomainCatalog {
  let result = domain_catalog()
  for entry in self.entries {
    ignore(result.add(entry.name, entry.domain.clone(), entry.tag))
  }
  result
}

///|
/// Return whether every domain is non-empty.
pub fn DomainCatalog::consistent(self : DomainCatalog) -> Bool {
  self.empty_entries().length() == 0
}

///|
/// Return the number of available values.
pub fn DomainCatalog::available_values(self : DomainCatalog) -> Int {
  let mut result = 0
  for entry in self.entries {
    result += entry.domain.size()
  }
  result
}

///|
/// Return domains with at most a threshold number of values.
pub fn DomainCatalog::most_constrained(
  self : DomainCatalog,
  threshold : Int,
) -> Array[Int] {
  let result : Array[Int] = []
  for entry in self.entries {
    if entry.domain.size() <= threshold {
      result.push(entry.id)
    }
  }
  result
}

///|
/// Return a compact catalog description.
pub fn DomainCatalog::describe(self : DomainCatalog) -> String {
  let builder = StringBuilder()
  for index, entry in self.entries {
    if index > 0 {
      builder.write_char('\n')
    }
    builder.write_string(
      "\{entry.id}:\{entry.name}[\{entry.domain.describe()}]/\{entry.tag}",
    )
  }
  builder.to_string()
}

///|
/// Return a catalog fingerprint.
pub fn DomainCatalog::signature(self : DomainCatalog) -> Int {
  let mut result = 31
  for entry in self.entries {
    result = result * 37 + entry.id + entry.domain.size() * 7
    for character in entry.name {
      result = result * 41 + character.to_int()
    }
  }
  result
}

///|
/// Return the union of all values in tagged entries.
pub fn DomainCatalog::tagged_values(
  self : DomainCatalog,
  tag : String,
) -> IntegerSet {
  let values : Array[Int] = []
  for entry in self.entries {
    if entry.tag == tag {
      for value in entry.domain.values() {
        values.push(value)
      }
    }
  }
  integer_set(values)
}

///|
/// Return the smallest enclosing interval for tagged domains.
pub fn DomainCatalog::tagged_bounds(
  self : DomainCatalog,
  tag : String,
) -> (Int, Int)? {
  let values = self.tagged_values(tag).values()
  if values.length() == 0 {
    return None
  }
  Some((values[0], values[values.length() - 1]))
}

///|
/// Return the number of holes across all domains.
pub fn DomainCatalog::hole_count(self : DomainCatalog) -> Int {
  let mut result = 0
  for entry in self.entries {
    result += entry.domain.removed_count()
  }
  result
}

///|
/// Return a copy with every domain intersected by a shared interval.
pub fn DomainCatalog::restrict_all(
  self : DomainCatalog,
  lower : Int,
  upper : Int,
) -> Int {
  let mut changed = 0
  for entry in self.entries {
    if entry.domain.intersect_range(lower, upper) {
      changed += 1
    }
  }
  changed
}