use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use ifc_model::{Entity, EntityId, Model, Value};
use super::super::layout::Layout;
use super::finding::UndecidedReason;
use crate::error::PropertyAnomaly;
pub(super) fn carriers(
model: &Model,
layout: Layout,
templated: &BTreeSet<EntityId>,
anomalies: &mut Vec<PropertyAnomaly>,
) -> BTreeMap<EntityId, BTreeSet<EntityId>> {
let mut out: BTreeMap<EntityId, BTreeSet<EntityId>> = BTreeMap::new();
let schema = layout.schema();
let mut relationships = vec!["IFCRELDEFINESBYPROPERTIES"];
relationships.extend(schema.subtypes("IFCRELDEFINESBYPROPERTIES"));
for relationship in relationships {
for &id in model.ids_of_type(relationship) {
let Some(rel) = model.get(id) else { continue };
let definitions = definition_refs(layout.get(rel, "RelatingPropertyDefinition"));
let sets: Vec<EntityId> = definitions
.into_iter()
.filter(|set| templated.contains(set))
.collect();
if sets.is_empty() {
continue;
}
for object in list_refs(layout.get(rel, "RelatedObjects")) {
if model.get(object).is_none() {
anomalies.push(PropertyAnomaly::MissingObject {
relationship: id,
object,
});
continue;
}
for set in &sets {
out.entry(*set).or_default().insert(object);
}
}
}
}
for (type_name, _) in model.type_histogram() {
if !layout.is_a(type_name, "IFCTYPEOBJECT") {
continue;
}
for &id in model.ids_of_type(type_name) {
let Some(type_object) = model.get(id) else {
continue;
};
for set in list_refs(layout.get(type_object, "HasPropertySets")) {
if templated.contains(&set) {
out.entry(set).or_default().insert(id);
}
}
}
}
out
}
fn definition_refs(value: Option<&Value>) -> Vec<EntityId> {
match value.map(Value::unwrap_typed) {
Some(Value::Ref(id)) => vec![*id],
Some(list @ Value::List(_)) => list_refs(Some(list)),
_ => Vec::new(),
}
}
fn list_refs(value: Option<&Value>) -> Vec<EntityId> {
match value {
Some(Value::List(items)) => items.iter().filter_map(Value::as_ref_id).collect(),
_ => Vec::new(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Verdict {
Admits,
Excludes,
Undecided(UndecidedReason),
}
pub(super) fn applicable(layout: Layout, object: &Entity, applicable_entity: &str) -> Verdict {
let mut undecided = None;
for entry in applicable_entity.split(',') {
match admits(layout, object, entry.trim()) {
Verdict::Admits => return Verdict::Admits,
Verdict::Undecided(reason) => {
undecided.get_or_insert(reason);
}
Verdict::Excludes => {}
}
}
undecided.map_or(Verdict::Excludes, Verdict::Undecided)
}
struct Entry<'a> {
entity: &'a str,
predefined: Option<&'a str>,
performance_history: bool,
}
const PERFORMANCE_HISTORY: &str = "[PerformanceHistory]";
fn parse(entry: &str) -> Option<Entry<'_>> {
let split = entry.len().checked_sub(PERFORMANCE_HISTORY.len());
let (body, performance_history) = match split.and_then(|at| entry.split_at_checked(at)) {
Some((body, suffix)) if suffix.eq_ignore_ascii_case(PERFORMANCE_HISTORY) => (body, true),
_ => (entry, false),
};
let (entity, predefined) = match body.split_once('/') {
Some((entity, predefined)) => (entity, Some(predefined)),
None => (body, None),
};
let word = |text: &str| {
!text.is_empty() && text.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
};
if !word(entity) || predefined.is_some_and(|p| !word(p)) {
return None;
}
Some(Entry {
entity,
predefined,
performance_history,
})
}
fn admits(layout: Layout, object: &Entity, text: &str) -> Verdict {
let undecided =
|reason: fn(Arc<str>) -> UndecidedReason| Verdict::Undecided(reason(text.into()));
let Some(entry) = parse(text) else {
return undecided(|entry| UndecidedReason::UnknownApplicableEntity { entry });
};
if layout.schema().entity(entry.entity).is_none() {
return undecided(|entry| UndecidedReason::UnknownApplicableEntity { entry });
}
if entry.performance_history {
return if layout.is_a(&object.type_name, "IFCPERFORMANCEHISTORY") {
undecided(|entry| UndecidedReason::PerformanceHistory { entry })
} else {
Verdict::Excludes
};
}
if !layout.is_a(&object.type_name, entry.entity) {
return Verdict::Excludes;
}
let Some(predefined) = entry.predefined else {
return Verdict::Admits;
};
match layout
.get(object, "PredefinedType")
.map(Value::unwrap_typed)
{
Some(Value::Enum(stated)) if stated.eq_ignore_ascii_case(predefined) => Verdict::Admits,
Some(Value::Enum(stated)) if !stated.eq_ignore_ascii_case("NOTDEFINED") => {
Verdict::Excludes
}
_ => undecided(|entry| UndecidedReason::PredefinedTypeUnstated { entry }),
}
}