use std::collections::HashSet;
use ifc_model::guid::Guid;
use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::Schema;
use crate::authoring::ControlKind;
use crate::error::{ControlError, ControlResult};
const RELATION: &str = "IFCRELASSIGNSTOCONTROL";
#[derive(Debug, Clone, Copy)]
pub struct ControlAssignmentDraft<'a> {
pub global_id: &'a str,
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub control: EntityId,
pub related_objects: &'a [EntityId],
}
pub fn assign_to_control(
tx: &mut Transaction,
model: &Model,
schema: &Schema,
draft: ControlAssignmentDraft<'_>,
) -> ControlResult<EntityId> {
let names = schema.attribute_names(RELATION);
let slot = |attribute: &str| names.iter().position(|n| *n == attribute);
let (Some(related_slot), Some(type_slot), Some(relating_slot)) = (
slot("RelatedObjects"),
slot("RelatedObjectsType"),
slot("RelatingControl"),
) else {
return Err(ControlError::UnsupportedEntity {
schema: schema.name().to_owned(),
entity: RELATION,
});
};
if Guid::parse(draft.global_id).is_none() {
return Err(invalid("GlobalId", draft.global_id));
}
let control_type = projected_type(tx, model, draft.control)
.ok_or(ControlError::UnknownEntity { id: draft.control })?;
if !ControlKind::ALL
.iter()
.any(|kind| control_type.eq_ignore_ascii_case(kind.type_name()))
{
return Err(ControlError::ForeignControl {
id: draft.control,
actual: control_type,
});
}
if draft.related_objects.is_empty() {
return Err(invalid("RelatedObjects", "expected at least one member"));
}
let mut seen = HashSet::new();
for &object in draft.related_objects {
if !seen.insert(object) {
return Err(invalid(
"RelatedObjects",
format!("{object} is listed twice"),
));
}
if object == draft.control {
return Err(invalid(
"RelatedObjects",
format!("{object} is the relating control"),
));
}
let actual =
projected_type(tx, model, object).ok_or(ControlError::UnknownEntity { id: object })?;
if !schema.is_a(&actual, "IFCOBJECTDEFINITION") {
return Err(invalid(
"RelatedObjects",
format!("{object} is {actual}, not an IfcObjectDefinition"),
));
}
}
let mut attrs = vec![Value::Null; names.len()];
attrs[0] = Value::Text(draft.global_id.into());
attrs[2] = text(draft.name);
attrs[3] = text(draft.description);
attrs[related_slot] = Value::List(
draft
.related_objects
.iter()
.copied()
.map(Value::Ref)
.collect(),
);
attrs[type_slot] = Value::Null;
attrs[relating_slot] = Value::Ref(draft.control);
Ok(tx.create(Entity::new(RELATION, attrs)))
}
fn projected_type(tx: &Transaction, model: &Model, target: EntityId) -> Option<String> {
for edit in tx.edits().iter().rev() {
match edit {
Edit::Remove { id } if *id == target => return None,
Edit::Retype { id, type_name } if *id == target => return Some(type_name.to_string()),
Edit::Create { id, entity } if *id == target => {
return Some(entity.type_name.to_string())
}
_ => {}
}
}
model.get(target).map(|entity| entity.type_name.to_string())
}
fn invalid(attribute: &'static str, value: impl Into<String>) -> ControlError {
ControlError::AuthoringInvalid {
entity: RELATION,
attribute,
value: value.into(),
}
}
fn text(value: Option<&str>) -> Value {
value.map_or(Value::Null, |v| Value::Text(v.into()))
}