use std::fmt;
use std::future::Future;
use std::sync::{Arc, Weak};
use hidpp::channel::HidppChannel;
use thiserror::Error;
use crate::{ChannelRegistry, DeviceRoute, SharedChannel};
pub trait RestorePlan {
fn restore_on(&self, current: &SharedChannel) -> impl Future<Output = bool> + Send;
}
#[derive(Clone, Copy)]
enum RetiredChannelPolicy {
ReplacementOnly,
CurrentAllowed,
}
pub struct PendingRestore<P> {
route: DeviceRoute,
retired_channel: Weak<HidppChannel>,
retired_policy: RetiredChannelPolicy,
plan: P,
}
impl<P: fmt::Debug> fmt::Debug for PendingRestore<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PendingRestore")
.field("route", &self.route)
.field("plan", &self.plan)
.finish_non_exhaustive()
}
}
impl<P> PendingRestore<P> {
pub(crate) fn owing(retired: &SharedChannel, plan: P) -> Self {
Self {
route: retired.route().clone(),
retired_channel: Arc::downgrade(retired.channel()),
retired_policy: RetiredChannelPolicy::ReplacementOnly,
plan,
}
}
pub(crate) fn allow_current_channel(mut self) -> Self {
self.retired_policy = RetiredChannelPolicy::CurrentAllowed;
self
}
}
impl<P: RestorePlan> PendingRestore<P> {
pub async fn retry(self, registry: &ChannelRegistry) -> RestoreOutcome<P> {
let Some(current) = registry.lookup(&self.route) else {
return RestoreOutcome::RestorePending(self);
};
if matches!(self.retired_policy, RetiredChannelPolicy::ReplacementOnly)
&& self
.retired_channel
.upgrade()
.is_some_and(|retired| Arc::ptr_eq(current.channel(), &retired))
{
return RestoreOutcome::RestorePending(self);
}
let restored = self.plan.restore_on(¤t).await;
if restored && registry.is_current(¤t) {
RestoreOutcome::Restored
} else {
RestoreOutcome::RestorePending(self)
}
}
}
#[must_use = "a pending restore must be retained until firmware ownership is released"]
pub enum RestoreOutcome<P> {
Restored,
RestorePending(PendingRestore<P>),
}
#[derive(Debug, Error)]
#[error("{error}")]
pub struct SessionFailure<E, P> {
#[source]
error: E,
pending_restore: Option<Box<PendingRestore<P>>>,
}
impl<E, P> SessionFailure<E, P> {
pub(crate) fn clean(error: E) -> Self {
Self {
error,
pending_restore: None,
}
}
pub(crate) fn with_pending(error: E, pending: PendingRestore<P>) -> Self {
Self {
error,
pending_restore: Some(Box::new(pending)),
}
}
#[must_use]
pub fn into_parts(self) -> (E, Option<PendingRestore<P>>) {
(self.error, self.pending_restore.map(|pending| *pending))
}
}
impl<E, P> From<E> for SessionFailure<E, P> {
fn from(error: E) -> Self {
Self::clean(error)
}
}
pub(crate) async fn rollback_start<E, P: RestorePlan>(
error: E,
pending: Option<PendingRestore<P>>,
registry: &ChannelRegistry,
) -> SessionFailure<E, P> {
let Some(pending) = pending else {
return SessionFailure::clean(error);
};
match pending.allow_current_channel().retry(registry).await {
RestoreOutcome::Restored => SessionFailure::clean(error),
RestoreOutcome::RestorePending(pending) => SessionFailure::with_pending(error, pending),
}
}