use ifc_model::guid::Guid;
use ifc_model::{EntityId, Model, Transaction, Value};
use ifc_schema::{Schema, TypeKind};
use super::{build_named, optional_text, validate_optional_ref};
use crate::error::{StructuralError, StructuralResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LoadGroupKind {
Group {
predefined_type: &'static str,
},
Case {
self_weight_coefficients: Option<[f64; 3]>,
},
}
#[derive(Debug, Clone)]
pub struct LoadGroupDraft {
pub global_id: String,
pub owner_history: Option<EntityId>,
pub name: Option<String>,
pub description: Option<String>,
pub object_type: Option<String>,
pub action_type: &'static str,
pub action_source: &'static str,
pub coefficient: Option<f64>,
pub purpose: Option<String>,
pub kind: LoadGroupKind,
}
pub fn stage_load_group(
tx: &mut Transaction,
model: &Model,
schema: &Schema,
draft: LoadGroupDraft,
) -> StructuralResult<EntityId> {
if Guid::parse(&draft.global_id).is_none() {
return Err(StructuralError::InvalidGlobalId);
}
let entity_type = match draft.kind {
LoadGroupKind::Group { .. } => "IfcStructuralLoadGroup",
LoadGroupKind::Case { .. } => "IfcStructuralLoadCase",
};
let predefined_type = match draft.kind {
LoadGroupKind::Group { predefined_type } => {
if predefined_type.eq_ignore_ascii_case("LOAD_CASE") {
return Err(StructuralError::SemanticViolation {
entity: None,
rule: "LOAD_CASE PredefinedType requires IfcStructuralLoadCase",
});
}
predefined_type
}
LoadGroupKind::Case { .. } => "LOAD_CASE",
};
for (attribute, token) in [
("PredefinedType", predefined_type),
("ActionType", draft.action_type),
("ActionSource", draft.action_source),
] {
validate_enum_token(schema, entity_type, attribute, token)?;
}
let user_defined = [predefined_type, draft.action_type, draft.action_source]
.iter()
.any(|token| token.eq_ignore_ascii_case("USERDEFINED"));
if user_defined
&& draft
.object_type
.as_deref()
.is_none_or(|value| value.trim().is_empty())
{
return Err(StructuralError::SemanticViolation {
entity: None,
rule: "USERDEFINED load group requires an ObjectType",
});
}
validate_finite(draft.coefficient, entity_type, "Coefficient")?;
let self_weight = match draft.kind {
LoadGroupKind::Case {
self_weight_coefficients: Some(ratios),
} => {
for ratio in ratios {
validate_finite(Some(ratio), entity_type, "SelfWeightCoefficients")?;
}
Some(Value::List(
ratios.iter().copied().map(Value::Real).collect(),
))
}
_ => None,
};
validate_root_refs(tx, model, schema, draft.owner_history)?;
let mut fields = vec![
("GlobalId", Value::Text(draft.global_id.into())),
("Name", optional_text(draft.name)),
("Description", optional_text(draft.description)),
("ObjectType", optional_text(draft.object_type)),
("PredefinedType", Value::Enum(predefined_type.into())),
("ActionType", Value::Enum(draft.action_type.into())),
("ActionSource", Value::Enum(draft.action_source.into())),
("Purpose", optional_text(draft.purpose)),
(
"Coefficient",
draft.coefficient.map_or(Value::Null, Value::Real),
),
];
if let Some(owner_history) = draft.owner_history {
fields.push(("OwnerHistory", Value::Ref(owner_history)));
}
if let Some(values) = self_weight {
fields.push(("SelfWeightCoefficients", values));
}
Ok(tx.create(build_named(schema, entity_type, fields)?))
}
pub(super) fn validate_enum_token(
schema: &Schema,
entity_type: &'static str,
attribute: &'static str,
token: &str,
) -> StructuralResult<()> {
let declared = schema
.attributes(entity_type)
.iter()
.find(|candidate| candidate.name.eq_ignore_ascii_case(attribute))
.and_then(|candidate| schema.type_def(&candidate.type_name))
.is_some_and(|definition| match &definition.kind {
TypeKind::Enumeration(values) => values.iter().any(|member| member == token),
_ => false,
});
if declared {
return Ok(());
}
Err(StructuralError::InvalidDraftValue {
entity_type,
attribute,
expected: "a token the schema declares for this attribute",
})
}
fn validate_finite(
value: Option<f64>,
entity_type: &'static str,
attribute: &'static str,
) -> StructuralResult<()> {
if value.is_some_and(|number| !number.is_finite()) {
return Err(StructuralError::InvalidDraftValue {
entity_type,
attribute,
expected: "a finite ratio",
});
}
Ok(())
}
fn validate_root_refs(
tx: &Transaction,
model: &Model,
schema: &Schema,
owner_history: Option<EntityId>,
) -> StructuralResult<()> {
validate_optional_ref(tx, model, schema, owner_history, "IfcOwnerHistory")
}