///|
/// An immutable in-memory feature/device catalog used by planning adapters.
pub(all) struct Catalog {
  features : Array[Feature]
  devices : Array[Device]
}

///|
/// Construct a catalog while preserving caller order.
pub fn catalog_from(
  features~ : Array[Feature],
  devices~ : Array[Device],
) -> Catalog {
  { features, devices }
}

///|
/// Find the first feature with the requested identifier.
pub fn find_feature(catalog : Catalog, id : String) -> Feature? {
  let mut result = None
  for feature in catalog.features {
    if feature.id == id && result is None {
      result = Some(feature)
    }
  }
  result
}

///|
/// Find the first device with the requested identifier.
pub fn find_device(catalog : Catalog, id : String) -> Device? {
  let mut result = None
  for device in catalog.devices {
    if device.id == id && result is None {
      result = Some(device)
    }
  }
  result
}

///|
/// Return features of one kind in source order.
pub fn features_of_kind(
  catalog : Catalog,
  kind : FeatureKind,
) -> Array[Feature] {
  let result = []
  for feature in catalog.features {
    if feature.kind == kind {
      result.push(feature)
    }
  }
  result
}

///|
/// Return datum features in source order.
pub fn datum_features(catalog : Catalog) -> Array[Feature] {
  let result = []
  for feature in catalog.features {
    if feature.datum {
      result.push(feature)
    }
  }
  result
}

///|
/// Count catalog features supported by a device.
pub fn supported_feature_count(
  device : Device,
  features : Array[Feature],
) -> Int {
  let mut result = 0
  for feature in features {
    if device.can_measure(feature) {
      result += 1
    }
  }
  result
}

///|
/// Return features with no reachable supporting device.
pub fn unmeasurable_features(catalog : Catalog) -> Array[Feature] {
  let result = []
  for feature in catalog.features {
    let mut supported = false
    for device in catalog.devices {
      if device.can_measure(feature) {
        supported = true
      }
    }
    if !supported {
      result.push(feature)
    }
  }
  result
}

///|
/// Return whether structural validation has no errors.
pub fn catalog_is_valid(catalog : Catalog) -> Bool {
  !has_errors(validate_catalog(catalog.features, catalog.devices))
}

///|
/// Return feature identifiers in source order.
pub fn catalog_feature_ids(catalog : Catalog) -> Array[String] {
  let result = []
  for feature in catalog.features {
    result.push(feature.id)
  }
  result
}

///|
/// Return device identifiers in source order.
pub fn catalog_device_ids(catalog : Catalog) -> Array[String] {
  let result = []
  for device in catalog.devices {
    result.push(device.id)
  }
  result
}

///|
/// Return the first index of a feature identifier, or -1.
pub fn feature_index(catalog : Catalog, id : String) -> Int {
  let mut result = -1
  for i, feature in catalog.features {
    if feature.id == id && result < 0 {
      result = i
    }
  }
  result
}

///|
/// Return the first index of a device identifier, or -1.
pub fn device_index(catalog : Catalog, id : String) -> Int {
  let mut result = -1
  for i, device in catalog.devices {
    if device.id == id && result < 0 {
      result = i
    }
  }
  result
}

///|
/// Return all devices that can measure a feature.
pub fn supported_devices(catalog : Catalog, feature : Feature) -> Array[Device] {
  let result = []
  for device in catalog.devices {
    if device.can_measure(feature) {
      result.push(device)
    }
  }
  result
}

///|
/// Return whether every catalog feature has a reachable device.
pub fn all_features_reachable(catalog : Catalog) -> Bool {
  unmeasurable_features(catalog).length() == 0
}

///|
/// Count the number of unique feature kinds present in a catalog.
pub fn catalog_kind_count(catalog : Catalog) -> Int {
  let kinds = []
  for feature in catalog.features {
    let mut seen = false
    for kind in kinds {
      if kind == feature.kind {
        seen = true
      }
    }
    if !seen {
      kinds.push(feature.kind)
    }
  }
  kinds.length()
}