use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use ifc_model::{Entity, EntityId, Model, Value};
use super::super::layout::Layout;
use super::finding::{MeasureRole, TemplateFinding, UndecidedReason};
use super::form::{measured, prescribed, Prescribed};
use crate::error::PropertyAnomaly;
use crate::nesting::Nesting;
use crate::template::{PropertyTemplate, PropertyTemplateKind};
const NESTED: [(&str, &str); 2] = [
("IFCCOMPLEXPROPERTY", "HasProperties"),
("IFCPHYSICALCOMPLEXQUANTITY", "HasQuantities"),
];
pub(super) struct Member<'m> {
id: EntityId,
entity: &'m Entity,
name: Option<Arc<str>>,
children: Vec<Member<'m>>,
}
pub(super) fn read_members<'m>(
model: &'m Model,
layout: Layout,
set: EntityId,
list: Option<&Value>,
attribute: &'static str,
nesting: &mut Nesting<'_>,
) -> Vec<Member<'m>> {
let mut members = Vec::new();
for member in nesting.members(set, attribute, list) {
if nesting.admit(model, set, member) {
members.push(read_member(model, layout, member, nesting));
}
}
members
}
fn read_member<'m>(
model: &'m Model,
layout: Layout,
id: EntityId,
nesting: &mut Nesting<'_>,
) -> Member<'m> {
let entity = model.get(id).expect("admitted members are in the model");
let mut found = Vec::new();
layout.check_arity(id, entity, &mut found);
let name = layout.text(id, entity, "Name", &mut found);
let unset = layout.get(entity, "Name").map(Value::unwrap_typed);
if matches!(unset, None | Some(Value::Null)) {
found.push(PropertyAnomaly::MalformedAttribute {
entity: id,
attribute: "Name",
found: "Null".to_owned(),
});
}
for anomaly in found {
nesting.report(anomaly);
}
let mut children = Vec::new();
if let Some((_, attribute)) = NESTED
.iter()
.find(|(complex, _)| layout.is_a(&entity.type_name, complex))
{
if nesting.enter(id) {
let list = layout.get(entity, attribute);
children = read_members(model, layout, id, list, attribute, nesting);
nesting.leave();
}
}
Member {
id,
entity,
name,
children,
}
}
pub(super) struct Comparison<'a> {
pub(super) model: &'a Model,
pub(super) layout: Layout,
pub(super) set: EntityId,
pub(super) template: EntityId,
pub(super) findings: &'a mut Vec<TemplateFinding>,
}
impl Comparison<'_> {
fn undecided(&mut self, subject: EntityId, reason: UndecidedReason) {
self.findings.push(TemplateFinding::Undecided {
set: self.set,
template: self.template,
subject,
reason,
});
}
pub(super) fn members(
&mut self,
container: EntityId,
members: &[Member<'_>],
templates: &[PropertyTemplate],
) {
let mut governing: BTreeMap<&str, &PropertyTemplate> = BTreeMap::new();
for template in templates {
match template.name.as_deref() {
Some(name) => {
governing.entry(name).or_insert(template);
}
None => self.undecided(template.id, UndecidedReason::UnnamedTemplate),
}
}
let mut present = BTreeSet::new();
for member in members {
let Some(name) = member.name.as_deref() else {
continue;
};
present.insert(name);
match governing.get(name) {
Some(template) => self.member(member, template),
None => self.findings.push(TemplateFinding::UnexpectedProperty {
set: self.set,
template: self.template,
container,
property: member.id,
name: name.into(),
}),
}
}
for template in templates {
let Some(name) = template.name.as_deref() else {
continue;
};
let governs = governing.get(name).is_some_and(|t| t.id == template.id);
if governs && !present.contains(name) {
self.findings.push(TemplateFinding::MissingProperty {
set: self.set,
template: self.template,
container,
property_template: template.id,
name: name.into(),
});
}
}
}
fn member(&mut self, member: &Member<'_>, template: &PropertyTemplate) {
let template_type = template.template_type.as_deref();
let expected = match prescribed(self.layout, template.kind, template_type) {
Prescribed::Entities(expected) => expected,
other => {
let value: Arc<str> = template_type.unwrap_or_default().into();
let reason = if other == Prescribed::Unknown {
UndecidedReason::UnknownTemplateType { value }
} else {
UndecidedReason::UndocumentedTemplateType { value }
};
self.undecided(template.id, reason);
return;
}
};
let found = &member.entity.type_name;
if !expected
.iter()
.any(|entity| self.layout.is_a(found, entity))
{
self.findings.push(TemplateFinding::WrongForm {
set: self.set,
template: self.template,
property: member.id,
property_template: template.id,
template_type: template.template_type.clone(),
expected,
found: found.clone(),
});
return;
}
if let Some(template_type) = template_type {
self.measure(member, template, template_type, MeasureRole::Primary);
self.measure(member, template, template_type, MeasureRole::Secondary);
}
if template.kind == PropertyTemplateKind::Complex && !template.templates.is_empty() {
self.members(member.id, &member.children, &template.templates);
}
}
fn measure(
&mut self,
member: &Member<'_>,
template: &PropertyTemplate,
template_type: &str,
measure: MeasureRole,
) {
let label = match measure {
MeasureRole::Primary => &template.primary_measure,
MeasureRole::Secondary => &template.secondary_measure,
};
let Some(expected) = label else {
return;
};
let attributes = measured(template_type, measure);
if attributes.is_empty() {
return;
}
let schema = self.layout.schema();
if schema.type_def(expected).is_none() && schema.entity(expected).is_none() {
let value = expected.clone();
self.undecided(
template.id,
UndecidedReason::UnknownMeasureType { measure, value },
);
return;
}
for &attribute in attributes {
let Some(value) = self.layout.get(member.entity, attribute) else {
continue;
};
match self.nonconforming(expected, value) {
Ok(None) => {}
Ok(Some(found)) => self.findings.push(TemplateFinding::WrongMeasureType {
set: self.set,
template: self.template,
property: member.id,
property_template: template.id,
measure,
attribute,
expected: expected.clone(),
found,
}),
Err(()) => self.undecided(member.id, UndecidedReason::UntypedValue { attribute }),
}
}
}
fn nonconforming(&self, expected: &str, value: &Value) -> Result<Option<Arc<str>>, ()> {
let items = match value {
Value::List(items) => items.as_slice(),
single => std::slice::from_ref(single),
};
let schema = self.layout.schema();
for item in items {
let found = match item {
Value::Null => continue,
Value::Typed { type_name, .. } => type_name.clone(),
Value::Ref(id) => self.model.get(*id).ok_or(())?.type_name.clone(),
_ => return Err(()),
};
if !schema.accepts_type(expected, &found) {
return Ok(Some(found));
}
}
Ok(None)
}
}