use bevy::ecs::reflect::ReflectResource;
use bevy::ecs::system::Commands;
use bevy::ecs::system::Res;
use bevy::ecs::system::ResMut;
use bevy::prelude::Reflect;
use bevy::prelude::Resource;
use crate::Bindings;
use crate::DeviceIdSource;
use crate::DeviceKey;
use crate::DeviceResolution;
use crate::DeviceStateLookup;
use crate::Devices;
use crate::HardwareInventory;
use crate::IdentityDecisionOwed;
use crate::IdentityQuestionExpired;
use crate::IdentityQuestionRaised;
use crate::IdentityVerdict;
use crate::Presence;
use crate::RiggingRevision;
use crate::RoleKey;
use crate::binding::BindingError;
use crate::binding::EndpointOwner;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
pub enum IdentityQuestionState {
#[default]
Unseen,
Deferred,
}
#[derive(Debug, Reflect)]
pub struct IdentityQuestion {
pub role: RoleKey,
pub saved: DeviceKey,
pub candidate: DeviceKey,
pub arose: RiggingRevision,
pub state: IdentityQuestionState,
owner: EndpointOwner,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
pub enum IdentityAnswer {
Adopt,
Reject,
}
#[derive(Debug)]
pub enum IdentityQuestionLookup<'a> {
NoQuestion,
Pending(&'a IdentityQuestion),
}
#[derive(Clone, Debug, PartialEq, Eq, Reflect)]
pub enum AdoptionOutcome {
Adopted,
Refused,
CandidateEndpointOwned {
by: RoleKey,
},
NoSuchQuestion,
}
#[derive(Debug)]
pub enum IdentityAdoptionPreparation {
Prepared(PreparedIdentityAdoption),
Refused(AdoptionOutcome),
}
#[derive(Debug)]
pub struct PreparedIdentityAdoption {
role: RoleKey,
saved: DeviceKey,
candidate: DeviceKey,
device_roles: Vec<RoleKey>,
question_roles: Vec<RoleKey>,
}
struct RecordedAnswer {
role: RoleKey,
saved: DeviceKey,
candidate: DeviceKey,
answer: IdentityAnswer,
state: IdentityQuestionState,
arose: RiggingRevision,
}
struct SettledCandidate {
role: RoleKey,
saved: DeviceKey,
candidate: DeviceKey,
answer: IdentityAnswer,
discharge: IdentityDebtDischarge,
}
struct IdentityAdoptionQuestionSet {
positions: Vec<usize>,
roles: Vec<RoleKey>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum IdentityDebtDischarge {
Owed,
Done,
}
#[derive(Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct IdentityDecisions {
questions: Vec<IdentityQuestion>,
#[reflect(ignore, default = "Vec::new")]
recorded: Vec<RecordedAnswer>,
#[reflect(ignore, default = "Vec::new")]
settled: Vec<SettledCandidate>,
}
impl IdentityDecisions {
#[must_use]
pub fn questions(&self) -> &[IdentityQuestion] { &self.questions }
#[must_use]
pub fn question(&self, role: &RoleKey) -> IdentityQuestionLookup<'_> {
self.questions
.iter()
.find(|question| &question.role == role)
.map_or(IdentityQuestionLookup::NoQuestion, |question| {
IdentityQuestionLookup::Pending(question)
})
}
pub fn answer(
&mut self,
role: &RoleKey,
candidate: &DeviceKey,
identity_answer: IdentityAnswer,
) -> AdoptionOutcome {
if identity_answer == IdentityAnswer::Adopt {
return match self.prepare_device_adoption(role, candidate, std::iter::once(role)) {
IdentityAdoptionPreparation::Prepared(prepared) => {
self.answer_prepared_adoption(prepared)
},
IdentityAdoptionPreparation::Refused(outcome) => outcome,
};
}
let Some(position) = self
.questions
.iter()
.position(|question| &question.role == role && &question.candidate == candidate)
else {
return AdoptionOutcome::NoSuchQuestion;
};
let question = self.questions.remove(position);
self.recorded.push(RecordedAnswer {
role: question.role,
saved: question.saved,
candidate: question.candidate,
answer: identity_answer,
state: question.state,
arose: question.arose,
});
AdoptionOutcome::Refused
}
pub fn prepare_device_adoption<'a>(
&self,
role: &RoleKey,
candidate: &DeviceKey,
device_roles: impl IntoIterator<Item = &'a RoleKey>,
) -> IdentityAdoptionPreparation {
let mut device_role_set = Vec::new();
for device_role in device_roles {
if !device_role_set.contains(device_role) {
device_role_set.push(device_role.clone());
}
}
if !device_role_set.contains(role) {
return IdentityAdoptionPreparation::Refused(AdoptionOutcome::NoSuchQuestion);
}
let Some(selected_question) = self
.questions
.iter()
.find(|question| &question.role == role && &question.candidate == candidate)
else {
return IdentityAdoptionPreparation::Refused(AdoptionOutcome::NoSuchQuestion);
};
let saved = selected_question.saved.clone();
let candidate = selected_question.candidate.clone();
let question_set =
match self.adoption_question_set(role, &saved, &candidate, &device_role_set) {
Ok(question_set) => question_set,
Err(outcome) => return IdentityAdoptionPreparation::Refused(outcome),
};
IdentityAdoptionPreparation::Prepared(PreparedIdentityAdoption {
role: role.clone(),
saved,
candidate,
device_roles: device_role_set,
question_roles: question_set.roles,
})
}
pub fn answer_prepared_adoption(
&mut self,
prepared: PreparedIdentityAdoption,
) -> AdoptionOutcome {
let question_set = match self.adoption_question_set(
&prepared.role,
&prepared.saved,
&prepared.candidate,
&prepared.device_roles,
) {
Ok(question_set) => question_set,
Err(outcome) => return outcome,
};
if question_set.roles != prepared.question_roles {
return AdoptionOutcome::NoSuchQuestion;
}
let mut answered = Vec::with_capacity(question_set.positions.len());
for position in question_set.positions.into_iter().rev() {
answered.push(self.questions.remove(position));
}
for question in answered.into_iter().rev() {
self.recorded.push(RecordedAnswer {
role: question.role,
saved: question.saved,
candidate: question.candidate,
answer: IdentityAnswer::Adopt,
state: question.state,
arose: question.arose,
});
}
AdoptionOutcome::Adopted
}
pub fn defer(&mut self, role: &RoleKey, candidate: &DeviceKey) -> IdentityQuestionLookup<'_> {
let Some(question) = self
.questions
.iter_mut()
.find(|question| &question.role == role && &question.candidate == candidate)
else {
return IdentityQuestionLookup::NoQuestion;
};
question.state = IdentityQuestionState::Deferred;
IdentityQuestionLookup::Pending(question)
}
fn is_settled(&self, role: &RoleKey, candidate: &DeviceKey) -> bool {
self.settled
.iter()
.any(|settled| &settled.role == role && &settled.candidate == candidate)
}
fn any_question_about(&self, candidate: &DeviceKey) -> bool {
self.questions
.iter()
.any(|question| &question.candidate == candidate)
}
fn is_recorded(&self, role: &RoleKey, candidate: &DeviceKey) -> bool {
self.recorded
.iter()
.any(|recorded| &recorded.role == role && &recorded.candidate == candidate)
}
fn reinstate(&mut self, recorded_answer: RecordedAnswer, owner: EndpointOwner) {
let reinstated = IdentityQuestion {
role: recorded_answer.role,
saved: recorded_answer.saved,
candidate: recorded_answer.candidate,
arose: recorded_answer.arose,
state: recorded_answer.state,
owner,
};
let standing = self
.questions
.iter()
.position(|question| question.arose > reinstated.arose)
.unwrap_or(self.questions.len());
self.questions.insert(standing, reinstated);
}
fn forget_discharged(&mut self, discharged: &[DeviceKey]) {
self.settled.retain_mut(|settled| {
if settled.discharge == IdentityDebtDischarge::Done
|| !discharged.contains(&settled.candidate)
{
return true;
}
settled.discharge = IdentityDebtDischarge::Done;
settled.answer == IdentityAnswer::Reject
});
}
fn adoption_question_set(
&self,
role: &RoleKey,
saved: &DeviceKey,
candidate: &DeviceKey,
device_roles: &[RoleKey],
) -> Result<IdentityAdoptionQuestionSet, AdoptionOutcome> {
let selected_still_stands = self.questions.iter().any(|question| {
&question.role == role && &question.saved == saved && &question.candidate == candidate
});
if !selected_still_stands {
return Err(AdoptionOutcome::NoSuchQuestion);
}
let mut positions = Vec::new();
let mut roles = Vec::new();
for (position, question) in self.questions.iter().enumerate() {
if &question.saved != saved
|| &question.candidate != candidate
|| !device_roles.contains(&question.role)
{
continue;
}
if let EndpointOwner::OwnedBy(owner) = &question.owner {
return Err(AdoptionOutcome::CandidateEndpointOwned { by: owner.clone() });
}
positions.push(position);
roles.push(question.role.clone());
}
Ok(IdentityAdoptionQuestionSet { positions, roles })
}
}
pub(crate) fn adjudicate_identity_questions(
mut commands: Commands,
mut identity_decisions: ResMut<IdentityDecisions>,
mut bindings: ResMut<Bindings>,
mut devices: ResMut<Devices>,
mut hardware_inventory: ResMut<HardwareInventory>,
rigging_revision: Res<RiggingRevision>,
) {
if !identity_decisions.recorded.is_empty() {
apply_recorded_answers(
&mut commands,
&mut identity_decisions,
&mut bindings,
&mut hardware_inventory,
);
}
let unanswerable = unanswerable_questions(&identity_decisions, &bindings, &devices);
if !unanswerable.is_empty() {
expire_questions(&mut commands, &mut identity_decisions, &unanswerable);
}
let raise_pass =
raise_new_questions(&identity_decisions, &bindings, &devices, *rigging_revision);
for question in raise_pass.questions {
commands.trigger(IdentityQuestionRaised {
role: question.role.clone(),
candidate: question.candidate.clone(),
});
identity_decisions.questions.push(question);
}
for endpoint_owner_refresh in stale_endpoint_owners(&identity_decisions, &bindings) {
if let Some(question) = identity_decisions
.questions
.get_mut(endpoint_owner_refresh.standing)
{
question.owner = endpoint_owner_refresh.owner;
}
}
let settled_discharges = settled_discharges(&identity_decisions);
if !settled_discharges.is_empty() {
for candidate in &settled_discharges {
devices.discharge_identity_decision(candidate);
}
identity_decisions.forget_discharged(&settled_discharges);
}
for candidate in raise_pass.unanswerable {
devices.discharge_identity_decision(&candidate);
}
}
fn apply_recorded_answers(
commands: &mut Commands,
identity_decisions: &mut IdentityDecisions,
bindings: &mut Bindings,
hardware_inventory: &mut HardwareInventory,
) {
let mut still_waiting: Vec<RecordedAnswer> = Vec::new();
for recorded_answer in std::mem::take(&mut identity_decisions.recorded) {
match recorded_answer.answer {
IdentityAnswer::Adopt => {
match bindings.readdress(&recorded_answer.role, recorded_answer.candidate.clone()) {
Ok(()) => {
hardware_inventory
.readdress(&recorded_answer.saved, recorded_answer.candidate.clone());
identity_decisions.settled.push(SettledCandidate {
role: recorded_answer.role,
saved: recorded_answer.saved,
candidate: recorded_answer.candidate,
answer: IdentityAnswer::Adopt,
discharge: IdentityDebtDischarge::Owed,
});
},
Err(BindingError::EndpointAlreadyOwned { owner, .. }) => {
commands.trigger(IdentityQuestionRaised {
role: recorded_answer.role.clone(),
candidate: recorded_answer.candidate.clone(),
});
identity_decisions
.reinstate(recorded_answer, EndpointOwner::OwnedBy(owner));
},
Err(BindingError::PendingTransitionCapacityReached) => {
still_waiting.push(recorded_answer);
},
Err(_) => {
commands.trigger(IdentityQuestionExpired {
role: recorded_answer.role,
candidate: recorded_answer.candidate,
});
},
}
},
IdentityAnswer::Reject => {
identity_decisions.settled.push(SettledCandidate {
role: recorded_answer.role,
saved: recorded_answer.saved,
candidate: recorded_answer.candidate,
answer: IdentityAnswer::Reject,
discharge: IdentityDebtDischarge::Owed,
});
},
}
}
identity_decisions.recorded = still_waiting;
}
struct UnanswerableQuestion {
standing: usize,
}
fn unanswerable_questions(
identity_decisions: &IdentityDecisions,
bindings: &Bindings,
devices: &Devices,
) -> Vec<UnanswerableQuestion> {
identity_decisions
.questions
.iter()
.enumerate()
.filter(|(_, question)| {
let role_retired = bindings.binding(&question.role).is_err();
let candidate_hardware = candidate_hardware(devices, &question.candidate);
role_retired || candidate_hardware == CandidateHardware::Departed
})
.map(|(standing, _)| UnanswerableQuestion { standing })
.collect()
}
fn expire_questions(
commands: &mut Commands,
identity_decisions: &mut IdentityDecisions,
unanswerable: &[UnanswerableQuestion],
) {
let mut expired = Vec::with_capacity(unanswerable.len());
for unanswerable_question in unanswerable.iter().rev() {
expired.push(
identity_decisions
.questions
.remove(unanswerable_question.standing),
);
}
for question in expired.into_iter().rev() {
commands.trigger(IdentityQuestionExpired {
role: question.role,
candidate: question.candidate,
});
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CandidateHardware {
Present,
Departed,
}
fn candidate_hardware(devices: &Devices, candidate: &DeviceKey) -> CandidateHardware {
let DeviceResolution::Resolved(device_id) = devices.resolve(candidate) else {
return CandidateHardware::Departed;
};
let DeviceStateLookup::Retained(reconciled_device_state) = devices.state(device_id) else {
return CandidateHardware::Departed;
};
if reconciled_device_state.presence == Presence::Present {
CandidateHardware::Present
} else {
CandidateHardware::Departed
}
}
struct RaisePass {
questions: Vec<IdentityQuestion>,
unanswerable: Vec<DeviceKey>,
}
fn raise_new_questions(
identity_decisions: &IdentityDecisions,
bindings: &Bindings,
devices: &Devices,
rigging_revision: RiggingRevision,
) -> RaisePass {
let mut raised: Vec<IdentityQuestion> = Vec::new();
let mut unanswerable: Vec<DeviceKey> = Vec::new();
for reconciled_device_state in devices.states() {
let IdentityDecisionOwed::HumanDecision(outstanding_verdict) =
&reconciled_device_state.decision_owed
else {
continue;
};
if reconciled_device_state.presence != Presence::Present {
continue;
}
let Some(saved) = saved_key(outstanding_verdict) else {
continue;
};
if !answerable(saved) {
unanswerable.push(reconciled_device_state.key.clone());
continue;
}
let candidate = &reconciled_device_state.key;
let askable_roles = askable_roles(identity_decisions, bindings, saved);
if askable_roles.is_empty() {
unanswerable.push(candidate.clone());
continue;
}
for askable_role in askable_roles {
let role = &askable_role.role;
if identity_decisions.is_settled(role, candidate)
|| identity_decisions.is_recorded(role, candidate)
|| identity_decisions
.questions
.iter()
.chain(raised.iter())
.any(|question| &question.role == role && &question.candidate == candidate)
{
continue;
}
raised.push(IdentityQuestion {
role: askable_role.role.clone(),
saved: askable_role.saved,
candidate: candidate.clone(),
arose: rigging_revision,
state: IdentityQuestionState::Unseen,
owner: EndpointOwner::Unowned,
});
}
}
RaisePass {
questions: raised,
unanswerable,
}
}
#[derive(PartialEq, Eq)]
struct AskableRole {
role: RoleKey,
saved: DeviceKey,
}
fn askable_roles(
identity_decisions: &IdentityDecisions,
bindings: &Bindings,
saved: &DeviceKey,
) -> Vec<AskableRole> {
let mut askable: Vec<AskableRole> = bindings
.roles_for(saved)
.map(|role| AskableRole {
role: role.clone(),
saved: saved.clone(),
})
.collect();
for settled in &identity_decisions.settled {
if settled.answer != IdentityAnswer::Reject || &settled.candidate != saved {
continue;
}
if !bindings
.roles_for(&settled.saved)
.any(|role| role == &settled.role)
{
continue;
}
let inherited = AskableRole {
role: settled.role.clone(),
saved: settled.saved.clone(),
};
if !askable.contains(&inherited) {
askable.push(inherited);
}
}
askable
}
const fn answerable(saved: &DeviceKey) -> bool {
match saved.id {
DeviceIdSource::Reported { .. } | DeviceIdSource::Authored { .. } => true,
DeviceIdSource::Synthesized { .. } => false,
}
}
const fn saved_key(identity_verdict: &IdentityVerdict) -> Option<&DeviceKey> {
match identity_verdict {
IdentityVerdict::Displaced { saved } => Some(saved),
IdentityVerdict::WrongUnit { authored } => Some(authored),
_ => None,
}
}
struct EndpointOwnerRefresh {
standing: usize,
owner: EndpointOwner,
}
fn stale_endpoint_owners(
identity_decisions: &IdentityDecisions,
bindings: &Bindings,
) -> Vec<EndpointOwnerRefresh> {
identity_decisions
.questions
.iter()
.enumerate()
.filter_map(|(standing, question)| {
let owner = bindings.candidate_endpoint_owner(&question.role, &question.candidate);
(owner != question.owner).then_some(EndpointOwnerRefresh { standing, owner })
})
.collect()
}
fn settled_discharges(identity_decisions: &IdentityDecisions) -> Vec<DeviceKey> {
identity_decisions
.settled
.iter()
.filter(|settled| settled.discharge == IdentityDebtDischarge::Owed)
.map(|settled| settled.candidate.clone())
.filter(|candidate| !identity_decisions.any_question_about(candidate))
.collect()
}