use std::collections::BTreeSet;
use fhir_types::r4::coding::Coding;
use fhir_types::r4::primitives::Code;
use fhir_types::r4::primitives::String as FhirString;
use fhir_types::r4::primitives::Uri;
use openehr_base::v1_3::base_types::definitions::definitions_impl::LOCAL_TERMINOLOGY_ID;
use openehr_base::v1_3::base_types::identification::archetype_id::ArchetypeId;
use openehr_base::v1_3::base_types::identification::lexical::IdError;
use openehr_rm::v1_2::data_types::text::code_phrase::CodePhrase;
use openehr_rm::v1_2::data_types::text::dv_text::DvText;
use crate::terminology::Terminology;
use crate::terminology::TerminologyError;
use crate::terminology::ehn;
use crate::terminology::local::Term;
use crate::terminology::local::TermDefinitions;
use crate::terminology::system::fhir_system;
use crate::terminology::system::fhir_version;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreferredSystems {
systems: BTreeSet<String>,
}
impl Default for PreferredSystems {
fn default() -> Self {
Self::new(ehn::PATIENT_SUMMARY)
}
}
impl PreferredSystems {
#[must_use]
pub fn new<I, S>(systems: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
systems: systems.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn contains(&self, system: &str) -> bool {
self.systems.contains(system)
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.systems.iter().map(String::as_str)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Local<'a> {
terms: &'a TermDefinitions,
archetype: &'a ArchetypeId,
language: &'a str,
}
impl<'a> Local<'a> {
#[must_use]
pub const fn new(
terms: &'a TermDefinitions,
archetype: &'a ArchetypeId,
language: &'a str,
) -> Self {
Self {
terms,
archetype,
language,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Relation {
Broader,
Narrower,
Unknown,
}
impl Relation {
#[must_use]
pub const fn of(code: char) -> Option<Self> {
match code {
'>' => Some(Self::Broader),
'<' => Some(Self::Narrower),
'?' => Some(Self::Unknown),
_ => None,
}
}
#[must_use]
pub const fn words(self) -> &'static str {
match self {
Self::Broader => "broader term",
Self::Narrower => "narrower term",
Self::Unknown => "related term of unknown kind",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Transcoded {
codings: Vec<Coding>,
narrative: Vec<String>,
untranscoded: bool,
from_template: bool,
unmapped: Vec<Unmapped>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Unmapped {
terminology: String,
code: String,
}
impl Unmapped {
#[must_use]
pub fn terminology(&self) -> &str {
&self.terminology
}
#[must_use]
pub fn code(&self) -> &str {
&self.code
}
}
impl Transcoded {
#[must_use]
pub fn codings(&self) -> &[Coding] {
&self.codings
}
#[must_use]
pub fn narrative(&self) -> &[String] {
&self.narrative
}
#[must_use]
pub const fn untranscoded(&self) -> bool {
self.untranscoded
}
#[must_use]
pub const fn is_from_template(&self) -> bool {
self.from_template
}
#[must_use]
pub fn unmapped(&self) -> &[Unmapped] {
&self.unmapped
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TranscodeError {
#[error("a TERM_MAPPING carries a match outside '>', '=', '<' and '?'")]
InvalidMatch,
#[error("a CODE_PHRASE names no terminology or no code")]
Uncoded,
#[error("the archetype {archetype} defines no local code {code}")]
UndefinedLocalCode {
archetype: String,
code: String,
},
#[error("the archetype id {archetype} a local code is read in is not one")]
ArchetypeId {
archetype: String,
#[source]
source: IdError,
},
#[error("the terminology server gave no translation")]
Terminology(#[from] TerminologyError),
}
pub fn transcode(
text: &DvText,
preferred: &PreferredSystems,
) -> Result<Transcoded, TranscodeError> {
rules(text, preferred, None)
}
pub fn transcode_local(
text: &DvText,
preferred: &PreferredSystems,
local: &Local<'_>,
) -> Result<Transcoded, TranscodeError> {
rules(text, preferred, Some(local))
}
pub async fn transcode_with(
text: &DvText,
preferred: &PreferredSystems,
terminology: &Terminology,
) -> Result<Transcoded, TranscodeError> {
translated(transcode(text, preferred)?, preferred, terminology).await
}
pub async fn transcode_local_with(
text: &DvText,
preferred: &PreferredSystems,
local: &Local<'_>,
terminology: &Terminology,
) -> Result<Transcoded, TranscodeError> {
translated(
transcode_local(text, preferred, local)?,
preferred,
terminology,
)
.await
}
fn rules(
text: &DvText,
preferred: &PreferredSystems,
local: Option<&Local<'_>>,
) -> Result<Transcoded, TranscodeError> {
let (value, defining, mappings) = match text {
DvText::DvCodedText(coded) => (
coded.value.as_str(),
Some(&coded.defining_code),
coded.mappings.as_ref(),
),
DvText::DvText(plain) => (plain.value.as_str(), None, plain.mappings.as_ref()),
};
let archetype = local.map(|local| local.archetype);
let mut transcoded = Transcoded {
narrative: vec![value.to_owned()],
..Transcoded::default()
};
if let Some(defining) = defining {
let term = rendered(defining, local)?;
if let Some(term) = term {
transcoded.narrative = vec![term.to_owned()];
if term != value {
transcoded.narrative.push(value.to_owned());
}
transcoded.from_template = true;
}
let display = term.unwrap_or(value);
transcoded.place(defining, Some(display), archetype, || {
format!(
"{value}: {} {}",
defining.terminology_id.name(),
defining.code_string
)
})?;
}
for mapping in mappings.into_iter().flatten() {
let relation = match mapping.r#match {
'=' => {
let target = &mapping.target;
transcoded.place(target, target.preferred_term.as_deref(), archetype, || {
narrated(value, "equivalent term", target)
})?;
continue;
}
'>' => Relation::Broader,
'<' => Relation::Narrower,
'?' => Relation::Unknown,
_ => return Err(TranscodeError::InvalidMatch),
};
transcoded
.narrative
.push(narrated(value, relation.words(), &mapping.target));
}
transcoded.untranscoded = !transcoded.from_template
&& (!transcoded.codings.is_empty() || !transcoded.unmapped.is_empty())
&& !transcoded
.codings
.iter()
.any(|coding| system_of(coding).is_some_and(|system| preferred.contains(system)));
Ok(transcoded)
}
fn rendered<'a>(
defining: &CodePhrase,
local: Option<&Local<'a>>,
) -> Result<Option<&'a str>, TranscodeError> {
let Some(local) = local else {
return Ok(None);
};
if defining.terminology_id.name() != LOCAL_TERMINOLOGY_ID {
return Ok(None);
}
match local
.terms
.term(local.language, local.archetype, &defining.code_string)
{
Term::Defined(text) => Ok(Some(text)),
Term::OtherLanguage => Ok(None),
Term::Undefined => Err(TranscodeError::UndefinedLocalCode {
archetype: local.archetype.value.clone(),
code: defining.code_string.clone(),
}),
}
}
async fn translated(
mut transcoded: Transcoded,
preferred: &PreferredSystems,
terminology: &Terminology,
) -> Result<Transcoded, TranscodeError> {
if !transcoded.untranscoded || !terminology.is_configured() {
return Ok(transcoded);
}
let originals: Vec<(String, String)> = transcoded
.codings
.iter()
.filter_map(|coding| {
let system = system_of(coding)?;
let code = coding.code.as_ref()?.value.as_deref()?;
Some((system.to_owned(), code.to_owned()))
})
.collect();
let mut added = Vec::new();
for (system, code) in &originals {
for target in preferred.iter() {
for found in terminology.translate(system, code, target).await? {
if !transcoded.codings.contains(&found) && !added.contains(&found) {
added.push(found);
}
}
}
}
if !added.is_empty() {
transcoded.untranscoded = !added
.iter()
.any(|coding| system_of(coding).is_some_and(|system| preferred.contains(system)));
transcoded.codings.extend(added);
}
Ok(transcoded)
}
impl Transcoded {
fn place(
&mut self,
phrase: &CodePhrase,
display: Option<&str>,
archetype: Option<&ArchetypeId>,
line: impl FnOnce() -> String,
) -> Result<(), TranscodeError> {
if let Some(coding) = coding(phrase, display, archetype)? {
self.codings.push(coding);
} else {
self.narrative.push(line());
self.unmapped.push(Unmapped {
terminology: phrase.terminology_id.name().to_owned(),
code: phrase.code_string.clone(),
});
}
Ok(())
}
}
fn narrated(value: &str, words: &str, target: &CodePhrase) -> String {
let mut line = format!(
"{value}: {words} {} {}",
target.terminology_id.name(),
target.code_string
);
if let Some(term) = &target.preferred_term {
line.push_str(" (");
line.push_str(term);
line.push(')');
}
line
}
fn coding(
phrase: &CodePhrase,
display: Option<&str>,
archetype: Option<&ArchetypeId>,
) -> Result<Option<Coding>, TranscodeError> {
let name = phrase.terminology_id.name();
if name.is_empty() || phrase.code_string.is_empty() {
return Err(TranscodeError::Uncoded);
}
let system = fhir_system(name, archetype).map_err(|source| TranscodeError::ArchetypeId {
archetype: archetype.map_or_else(String::new, |archetype| archetype.value.clone()),
source,
})?;
let Some(system) = system else {
return Ok(None);
};
let version = fhir_version(&system, phrase.terminology_id.version_id()).map(FhirString::from);
Ok(Some(Coding {
system: Some(Uri::from(system)),
version,
code: Some(Code::from(phrase.code_string.as_str())),
display: display
.filter(|display| !display.is_empty())
.map(FhirString::from),
..Coding::default()
}))
}
fn system_of(coding: &Coding) -> Option<&str> {
coding.system.as_ref()?.value.as_deref()
}