use ifc_model::guid::Guid;
use ifc_model::{Entity, EntityId, Transaction, Value};
use ifc_schema::Schema;
use crate::error::{ControlError, ControlResult};
const PERMIT: &[&str] = &["ACCESS", "BUILDING", "WORK", "USERDEFINED", "NOTDEFINED"];
const PROJECT_ORDER: &[&str] = &[
"CHANGEORDER",
"MAINTENANCEWORKORDER",
"MOVEORDER",
"PURCHASEORDER",
"WORKORDER",
"USERDEFINED",
"NOTDEFINED",
];
const ACTION_REQUEST: &[&str] = &[
"EMAIL",
"FAX",
"PHONE",
"POST",
"VERBAL",
"USERDEFINED",
"NOTDEFINED",
];
const PERFORMANCE_HISTORY: &[&str] = &["USERDEFINED", "NOTDEFINED"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ControlKind {
Permit,
ProjectOrder,
ActionRequest,
PerformanceHistory,
}
impl ControlKind {
#[must_use]
pub const fn type_name(self) -> &'static str {
match self {
Self::Permit => "IFCPERMIT",
Self::ProjectOrder => "IFCPROJECTORDER",
Self::ActionRequest => "IFCACTIONREQUEST",
Self::PerformanceHistory => "IFCPERFORMANCEHISTORY",
}
}
#[must_use]
pub const fn members(self) -> &'static [&'static str] {
match self {
Self::Permit => PERMIT,
Self::ProjectOrder => PROJECT_ORDER,
Self::ActionRequest => ACTION_REQUEST,
Self::PerformanceHistory => PERFORMANCE_HISTORY,
}
}
const fn predefined_slot(self) -> usize {
match self {
Self::PerformanceHistory => 7,
_ => 6,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ControlDraft<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub object_type: Option<&'a str>,
pub identification: Option<&'a str>,
pub status: Option<&'a str>,
pub long_description: Option<&'a str>,
pub life_cycle_phase: Option<&'a str>,
}
fn invalid(
entity: &'static str,
attribute: &'static str,
value: impl Into<String>,
) -> ControlError {
ControlError::AuthoringInvalid {
entity,
attribute,
value: value.into(),
}
}
fn text(value: Option<&str>) -> Value {
value.map_or(Value::Null, |v| Value::Text(v.into()))
}
fn blank(value: Option<&str>) -> bool {
value.is_none_or(|v| v.trim().is_empty())
}
pub fn create_control(
tx: &mut Transaction,
schema: &Schema,
kind: ControlKind,
global_id: &str,
predefined_type: Option<&str>,
draft: ControlDraft<'_>,
) -> ControlResult<EntityId> {
let entity = kind.type_name();
let declared = schema.attributes(entity);
if declared.is_empty() {
return Err(ControlError::UnsupportedEntity {
schema: schema.name().to_owned(),
entity,
});
}
if Guid::parse(global_id).is_none() {
return Err(invalid(entity, "GlobalId", global_id));
}
if blank(draft.name) {
return Err(invalid(entity, "Name", "required"));
}
if let Some(token) = predefined_type {
if !kind.members().contains(&token) {
return Err(invalid(entity, "PredefinedType", token));
}
if token == "USERDEFINED" && blank(draft.object_type) {
return Err(invalid(entity, "ObjectType", "required by USERDEFINED"));
}
}
let history = kind == ControlKind::PerformanceHistory;
if history {
if blank(draft.life_cycle_phase) {
return Err(invalid(entity, "LifeCyclePhase", "required"));
}
if draft.status.is_some() {
return Err(invalid(entity, "Status", "not declared"));
}
if draft.long_description.is_some() {
return Err(invalid(entity, "LongDescription", "not declared"));
}
} else if draft.life_cycle_phase.is_some() {
return Err(invalid(entity, "LifeCyclePhase", "not declared"));
}
let mut attrs = vec![Value::Null; declared.len()];
attrs[0] = Value::Text(global_id.into());
attrs[2] = text(draft.name);
attrs[3] = text(draft.description);
attrs[4] = text(draft.object_type);
attrs[5] = text(draft.identification);
if history {
attrs[6] = text(draft.life_cycle_phase);
} else {
let tail = [
(7, "Status", draft.status),
(8, "LongDescription", draft.long_description),
];
for (slot, attribute, value) in tail {
if let Some(cell) = attrs.get_mut(slot) {
*cell = text(value);
} else if value.is_some() {
return Err(invalid(entity, attribute, "not declared"));
}
}
}
attrs[kind.predefined_slot()] = predefined_type.map_or(Value::Null, |t| Value::Enum(t.into()));
Ok(tx.create(Entity::new(entity, attrs)))
}