alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Pending and sealed effect batches.

use super::{
    DrainedPluginEffectReport, PluginBufferEditProposal, PluginEffect, PluginEffectBatchLimit,
    PluginEffectCommitError, PluginEffectDiscardReport,
};
use crate::{
    StatusInfoText,
    ecs::{
        components::buffer::ViewEntity,
        events::status::{StatusMessage, StatusMessageRequested},
    },
    plugin::PluginIdentity,
};
use std::fmt::{Debug, Formatter};

/// Effects accepted during one guest update.
#[derive(Eq, PartialEq)]
pub struct PendingPluginUpdateBatch {
    /// Stable plugin identity.
    identity: PluginIdentity,
    /// Per-update effect cap.
    limit: PluginEffectBatchLimit,
    /// Accepted but uncommitted effects.
    effects: Vec<PluginEffect>,
}

impl PendingPluginUpdateBatch {
    /// Creates an empty pending batch.
    #[must_use]
    pub const fn new(identity: PluginIdentity, limit: PluginEffectBatchLimit) -> Self {
        Self::from_validated_limit(identity, limit)
    }

    /// Creates an empty pending batch from an already-validated limit proof.
    #[must_use]
    pub(in crate::plugin) const fn from_validated_limit(
        identity: PluginIdentity,
        limit: PluginEffectBatchLimit,
    ) -> Self {
        Self {
            identity,
            limit,
            effects: Vec::new(),
        }
    }

    /// Returns the plugin identity for display and serialization.
    #[must_use]
    pub fn identity(&self) -> &str {
        self.identity_proof().as_str()
    }

    /// Returns the validated identity retained before the successful-return boundary.
    #[must_use]
    pub const fn identity_proof(&self) -> &PluginIdentity {
        &self.identity
    }

    /// Queues one already-normalized effect inside this boundary.
    fn push(&mut self, effect: PluginEffect) -> Result<(), PluginEffectCommitError> {
        if self.effects.len() >= self.limit.max_effects() {
            return Err(PluginEffectCommitError::TooManyEffects {
                identity: self.identity.clone(),
                limit: self.limit.max_effects_limit(),
            });
        }
        self.effects.push(effect);
        Ok(())
    }

    /// Queues a buffer edit proposal.
    ///
    /// # Errors
    ///
    /// Returns [`PluginEffectCommitError::TooManyEffects`] when full.
    pub fn propose_buffer_edit(
        &mut self,
        effect: PluginBufferEditProposal,
    ) -> Result<(), PluginEffectCommitError> {
        self.push(PluginEffect::BufferEdit(effect))
    }

    /// Queues escaped status text.
    ///
    /// # Errors
    ///
    /// Returns [`PluginEffectCommitError::TooManyEffects`] when full.
    pub fn push_status_text(
        &mut self,
        target: ViewEntity,
        text: impl AsRef<str>,
    ) -> Result<(), PluginEffectCommitError> {
        self.push(PluginEffect::Status(StatusMessageRequested {
            target,
            message: StatusMessage::Info(StatusInfoText::escaped_display(text)),
        }))
    }

    /// Returns the pending effect count.
    #[must_use]
    pub const fn effect_count(&self) -> usize {
        self.effects.len()
    }

    /// Seals after successful guest return.
    #[must_use]
    pub fn seal(self) -> SealedPluginEffectBatch {
        SealedPluginEffectBatch {
            identity: self.identity,
            effects: self.effects,
        }
    }

    /// Discards before successful guest return.
    #[must_use]
    pub fn discard(self) -> PluginEffectDiscardReport {
        PluginEffectDiscardReport::new(self.identity, self.effects.len())
    }
}

impl Debug for PendingPluginUpdateBatch {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("PendingPluginUpdateBatch")
            .field("identity", &self.identity)
            .field("limit", &self.limit)
            .field("effect_count", &self.effects.len())
            .field(
                "effect_shapes",
                &self
                    .effects
                    .iter()
                    .map(PluginEffect::shape)
                    .collect::<Vec<_>>(),
            )
            .finish()
    }
}

/// Effects from a successful guest update.
#[derive(Eq, PartialEq)]
pub struct SealedPluginEffectBatch {
    /// Stable plugin identity.
    identity: PluginIdentity,
    /// Effects ready for ECS publication.
    effects: Vec<PluginEffect>,
}

impl SealedPluginEffectBatch {
    /// Returns the plugin identity for display and serialization.
    #[must_use]
    pub fn identity(&self) -> &str {
        self.identity_proof().as_str()
    }

    /// Returns the identity proof retained across the successful-return boundary.
    #[must_use]
    pub const fn identity_proof(&self) -> &PluginIdentity {
        &self.identity
    }

    /// Returns the sealed effect count.
    #[must_use]
    pub const fn effect_count(&self) -> usize {
        self.effects.len()
    }

    /// Returns whether this sealed batch carries no owner work.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.effects.is_empty()
    }

    /// Discards sealed effects that will not be retried before ECS publication.
    #[must_use]
    pub fn discard(self) -> PluginEffectDiscardReport {
        PluginEffectDiscardReport::new(self.identity, self.effects.len())
    }

    /// Drains into ECS request messages.
    #[must_use]
    pub fn drain(self) -> DrainedPluginEffectReport {
        let mut buffer_edits = Vec::new();
        let mut status_messages = Vec::new();

        for effect in self.effects {
            match effect {
                PluginEffect::BufferEdit(effect) => {
                    buffer_edits.push(effect.into_request(&self.identity));
                }
                PluginEffect::Status(request) => status_messages.push(request),
            }
        }

        DrainedPluginEffectReport::from_requests(self.identity, buffer_edits, status_messages)
    }
}

impl Debug for SealedPluginEffectBatch {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("SealedPluginEffectBatch")
            .field("identity", &self.identity)
            .field("effect_count", &self.effects.len())
            .field(
                "effect_shapes",
                &self
                    .effects
                    .iter()
                    .map(PluginEffect::shape)
                    .collect::<Vec<_>>(),
            )
            .finish()
    }
}