///|
pub(all) enum RegistryChangeKind {
  Added
  Removed
  Updated
  Unchanged
} derive(Debug, Eq)

///|
pub(all) struct RegistryChange {
  id : String
  kind : RegistryChangeKind
  before_version : String
  after_version : String
  detail : String
} derive(Debug, Eq)

///|
pub fn registry_change_kind_label(kind : RegistryChangeKind) -> String {
  match kind {
    Added => "added"
    Removed => "removed"
    Updated => "updated"
    Unchanged => "unchanged"
  }
}

///|
pub fn metadata_same(
  left : InstrumentMetadata,
  right : InstrumentMetadata,
) -> Bool {
  left.id == right.id &&
  left.display_name == right.display_name &&
  left.category == right.category &&
  left.unit == right.unit &&
  left.minimum == right.minimum &&
  left.maximum == right.maximum &&
  left.source_url == right.source_url &&
  left.version == right.version &&
  left.caution == right.caution &&
  left.keywords == right.keywords
}

///|
pub fn compare_metadata(
  before : InstrumentMetadata?,
  after : InstrumentMetadata?,
  id : String,
) -> RegistryChange {
  match (before, after) {
    (None, Some(next)) =>
      {
        id,
        kind: Added,
        before_version: "none",
        after_version: next.version,
        detail: "instrument entered registry",
      }
    (Some(previous), None) =>
      {
        id,
        kind: Removed,
        before_version: previous.version,
        after_version: "none",
        detail: "instrument removed from registry",
      }
    (Some(previous), Some(next)) => {
      let same = metadata_same(previous, next)
      {
        id,
        kind: if same {
          Unchanged
        } else {
          Updated
        },
        before_version: previous.version,
        after_version: next.version,
        detail: if same {
          "metadata unchanged"
        } else {
          "metadata fields changed"
        },
      }
    }
    (None, None) =>
      {
        id,
        kind: Unchanged,
        before_version: "none",
        after_version: "none",
        detail: "no metadata in either registry",
      }
  }
}

///|
pub fn registry_changes(
  before : InstrumentRegistry,
  after : InstrumentRegistry,
) -> Array[RegistryChange] {
  let ids = registry_ids(before)
  for id in registry_ids(after) {
    if !ids.contains(id) {
      ids.push(id)
    }
  }
  ids.map(fn(id) {
    compare_metadata(registry_find(before, id), registry_find(after, id), id)
  })
}

///|
pub fn registry_added(changes : Array[RegistryChange]) -> Array[RegistryChange] {
  changes.filter(fn(change) { change.kind == Added })
}

///|
pub fn registry_removed(
  changes : Array[RegistryChange],
) -> Array[RegistryChange] {
  changes.filter(fn(change) { change.kind == Removed })
}

///|
pub fn registry_updated(
  changes : Array[RegistryChange],
) -> Array[RegistryChange] {
  changes.filter(fn(change) { change.kind == Updated })
}

///|
pub fn registry_unchanged(
  changes : Array[RegistryChange],
) -> Array[RegistryChange] {
  changes.filter(fn(change) { change.kind == Unchanged })
}

///|
pub fn render_registry_change(change : RegistryChange) -> String {
  change.id +
  " " +
  registry_change_kind_label(change.kind) +
  " " +
  change.before_version +
  " -> " +
  change.after_version +
  ": " +
  change.detail
}

///|
pub fn render_registry_changes(changes : Array[RegistryChange]) -> String {
  changes.map(render_registry_change).join(" | ")
}

///|
pub fn registry_category_count(
  registry : InstrumentRegistry,
  category : InstrumentCategory,
) -> Int {
  registry_category_entries(registry, category).length()
}

///|
pub fn registry_category_counts(registry : InstrumentRegistry) -> Array[String] {
  [
    instrument_category_label(EarlyWarning) +
    "=" +
    registry_category_count(registry, EarlyWarning).to_string(),
    instrument_category_label(Cardiorespiratory) +
    "=" +
    registry_category_count(registry, Cardiorespiratory).to_string(),
    instrument_category_label(Neurology) +
    "=" +
    registry_category_count(registry, Neurology).to_string(),
    instrument_category_label(Trauma) +
    "=" +
    registry_category_count(registry, Trauma).to_string(),
    instrument_category_label(Gastrointestinal) +
    "=" +
    registry_category_count(registry, Gastrointestinal).to_string(),
    instrument_category_label(RenalHepatic) +
    "=" +
    registry_category_count(registry, RenalHepatic).to_string(),
    instrument_category_label(Perioperative) +
    "=" +
    registry_category_count(registry, Perioperative).to_string(),
    instrument_category_label(ObstetricPediatric) +
    "=" +
    registry_category_count(registry, ObstetricPediatric).to_string(),
    instrument_category_label(Workflow) +
    "=" +
    registry_category_count(registry, Workflow).to_string(),
  ]
}

///|
pub fn registry_has_duplicate_ids(registry : InstrumentRegistry) -> Bool {
  let ids = []
  for entry in registry.entries {
    if ids.contains(entry.id) {
      return true
    }
    ids.push(entry.id)
  }
  false
}

///|
pub fn registry_is_release_ready(registry : InstrumentRegistry) -> Bool {
  registry_validate(registry) && !registry_has_duplicate_ids(registry)
}

///|
pub fn registry_filter_valid(
  registry : InstrumentRegistry,
) -> InstrumentRegistry {
  let entries = registry.entries.filter(validate_metadata)
  { entries, revision: registry.revision }
}

///|
pub fn registry_merge(
  primary : InstrumentRegistry,
  additions : InstrumentRegistry,
) -> InstrumentRegistry {
  let merged = primary.entries.copy()
  for candidate in additions.entries {
    let mut replaced = false
    for index = 0; index < merged.length(); index = index + 1 {
      if merged[index].id == candidate.id {
        merged[index] = candidate
        replaced = true
      }
    }
    if !replaced {
      merged.push(candidate)
    }
  }
  { entries: merged, revision: additions.revision }
}

///|
pub fn registry_select(
  registry : InstrumentRegistry,
  ids : Array[String],
) -> InstrumentRegistry {
  let entries = []
  for id in ids {
    match registry_find(registry, id) {
      Some(entry) => entries.push(entry)
      None => ()
    }
  }
  { entries, revision: registry.revision }
}

///|
pub fn registry_revision_label(registry : InstrumentRegistry) -> String {
  "registry/" + registry.revision + "/" + registry_size(registry).to_string()
}