//! The editor's central undo/redo command stack.
//!
//! Every editor mutation that should be undoable — a transform edit, a rename, a
//! spawn/despawn, and later sculpt/paint/scatter brush strokes — is expressed as
//! a [`Command`] and pushed onto a [`CommandStack`]. Routing *all* edits through
//! one stack is what makes undo/redo consistent across the whole editor, and it
//! is why the Stage-7 gizmos and every later tool get undo "for free".
//!
//! The stack is generic over the context `C` a command mutates (in the editor
//! that is the scene + editor state), which keeps it decoupled and unit-testable
//! against a trivial context.

use std::any::Any;

/// A reversible editor action over a context `C`.
///
/// A command must be able to [`apply`](Self::apply) its effect and exactly
/// [`undo`](Self::undo) it. Commands are stored boxed on the [`CommandStack`].
pub trait Command<C>: 'static {
    /// Performs the action, mutating `ctx`.
    fn apply(&mut self, ctx: &mut C);

    /// Reverses the action, restoring `ctx` to its pre-[`apply`](Self::apply) state.
    fn undo(&mut self, ctx: &mut C);

    /// A short human-readable label (shown in the Edit menu / history).
    fn label(&self) -> String;

    /// Upcast for [`merge`](Self::merge) to downcast a following command.
    /// Implement as `self`.
    fn as_any_mut(&mut self) -> &mut dyn Any;

    /// Tries to fold the immediately-following command `next` into this one so
    /// they share a single undo entry (e.g. every frame of a gizmo drag becomes
    /// one undoable move). Return `true` if absorbed; the default never merges.
    ///
    /// When merging, update `self` so that undoing it reverses *both* effects.
    fn merge(&mut self, next: &mut dyn Command<C>) -> bool {
        let _ = next;
        false
    }
}

/// A composite command: several commands grouped into one undo entry.
///
/// Applied front-to-back and undone back-to-front, so a multi-step operation
/// (e.g. "duplicate and offset") is a single, atomic undo.
pub struct Group<C> {
    label: String,
    commands: Vec<Box<dyn Command<C>>>,
}

impl<C: 'static> Group<C> {
    /// A new, empty group with the given label.
    pub fn new(label: impl Into<String>) -> Self {
        Self {
            label: label.into(),
            commands: Vec::new(),
        }
    }

    /// Adds a command to the group (not yet applied).
    pub fn push(&mut self, command: impl Command<C> + 'static) {
        self.commands.push(Box::new(command));
    }

    /// Whether the group has no commands.
    pub fn is_empty(&self) -> bool {
        self.commands.is_empty()
    }
}

impl<C: 'static> Command<C> for Group<C> {
    fn apply(&mut self, ctx: &mut C) {
        for command in &mut self.commands {
            command.apply(ctx);
        }
    }

    fn undo(&mut self, ctx: &mut C) {
        for command in self.commands.iter_mut().rev() {
            command.undo(ctx);
        }
    }

    fn label(&self) -> String {
        self.label.clone()
    }

    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
}

/// A bounded undo/redo stack of [`Command`]s over a context `C`.
///
/// Pushing a command applies it and clears the redo history. Capacity caps how
/// many undo entries are retained (oldest dropped first) so the history cannot
/// grow without bound.
pub struct CommandStack<C> {
    undo: Vec<Box<dyn Command<C>>>,
    redo: Vec<Box<dyn Command<C>>>,
    capacity: usize,
}

impl<C: 'static> CommandStack<C> {
    /// The default maximum number of retained undo entries.
    pub const DEFAULT_CAPACITY: usize = 256;

    /// A stack with the [default capacity](Self::DEFAULT_CAPACITY).
    pub fn new() -> Self {
        Self::with_capacity(Self::DEFAULT_CAPACITY)
    }

    /// A stack retaining at most `capacity` undo entries (minimum 1).
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            undo: Vec::new(),
            redo: Vec::new(),
            capacity: capacity.max(1),
        }
    }

    /// Applies `command` and records it, clearing the redo history.
    ///
    /// If the previous top entry [`merge`](Command::merge)s this command, the two
    /// share one undo entry instead of pushing a new one.
    pub fn push(&mut self, command: impl Command<C> + 'static, ctx: &mut C) {
        self.push_boxed(Box::new(command), ctx);
    }

    /// Applies and records an already-boxed command (e.g. a [`Group`]).
    pub fn push_boxed(&mut self, mut command: Box<dyn Command<C>>, ctx: &mut C) {
        command.apply(ctx);
        self.redo.clear();
        if let Some(top) = self.undo.last_mut() {
            if top.merge(command.as_mut()) {
                return;
            }
        }
        self.undo.push(command);
        while self.undo.len() > self.capacity {
            self.undo.remove(0);
        }
    }

    /// Undoes the most recent command, moving it to the redo history. Returns its
    /// label, or `None` if there was nothing to undo.
    pub fn undo(&mut self, ctx: &mut C) -> Option<String> {
        let mut command = self.undo.pop()?;
        command.undo(ctx);
        let label = command.label();
        self.redo.push(command);
        Some(label)
    }

    /// Redoes the most recently undone command. Returns its label, or `None`.
    pub fn redo(&mut self, ctx: &mut C) -> Option<String> {
        let mut command = self.redo.pop()?;
        command.apply(ctx);
        let label = command.label();
        self.undo.push(command);
        Some(label)
    }

    /// Whether there is anything to undo.
    pub fn can_undo(&self) -> bool {
        !self.undo.is_empty()
    }

    /// Whether there is anything to redo.
    pub fn can_redo(&self) -> bool {
        !self.redo.is_empty()
    }

    /// The label of the next undo, if any (for the Edit menu).
    pub fn undo_label(&self) -> Option<String> {
        self.undo.last().map(|c| c.label())
    }

    /// The label of the next redo, if any.
    pub fn redo_label(&self) -> Option<String> {
        self.redo.last().map(|c| c.label())
    }

    /// The number of retained undo entries.
    pub fn undo_depth(&self) -> usize {
        self.undo.len()
    }

    /// Clears all history (e.g. on project close).
    pub fn clear(&mut self) {
        self.undo.clear();
        self.redo.clear();
    }
}

impl<C: 'static> Default for CommandStack<C> {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A trivial context: a single integer the test commands mutate.
    type Ctx = i32;

    /// Adds `amount` to the context; undo subtracts it. Consecutive `Add`s merge
    /// into one undo entry (modeling a continuous drag).
    struct Add {
        amount: i32,
        mergeable: bool,
    }

    impl Add {
        fn new(amount: i32) -> Self {
            Self {
                amount,
                mergeable: true,
            }
        }
        fn standalone(amount: i32) -> Self {
            Self {
                amount,
                mergeable: false,
            }
        }
    }

    impl Command<Ctx> for Add {
        fn apply(&mut self, ctx: &mut Ctx) {
            *ctx += self.amount;
        }
        fn undo(&mut self, ctx: &mut Ctx) {
            *ctx -= self.amount;
        }
        fn label(&self) -> String {
            format!("Add {}", self.amount)
        }
        fn as_any_mut(&mut self) -> &mut dyn Any {
            self
        }
        fn merge(&mut self, next: &mut dyn Command<Ctx>) -> bool {
            if !self.mergeable {
                return false;
            }
            if let Some(other) = next.as_any_mut().downcast_mut::<Add>() {
                if other.mergeable {
                    // Fold next's effect into this entry: undoing reverses both.
                    self.amount += other.amount;
                    return true;
                }
            }
            false
        }
    }

    #[test]
    fn apply_undo_redo_round_trip() {
        let mut ctx: Ctx = 0;
        let mut stack = CommandStack::new();
        stack.push(Add::standalone(5), &mut ctx);
        stack.push(Add::standalone(3), &mut ctx);
        assert_eq!(ctx, 8);
        assert_eq!(stack.undo_depth(), 2);

        assert_eq!(stack.undo(&mut ctx).as_deref(), Some("Add 3"));
        assert_eq!(ctx, 5);
        assert_eq!(stack.undo(&mut ctx).as_deref(), Some("Add 5"));
        assert_eq!(ctx, 0);
        assert!(!stack.can_undo());

        assert_eq!(stack.redo(&mut ctx).as_deref(), Some("Add 5"));
        assert_eq!(ctx, 5);
        assert!(stack.can_redo());
    }

    #[test]
    fn pushing_clears_redo() {
        let mut ctx: Ctx = 0;
        let mut stack = CommandStack::new();
        stack.push(Add::standalone(1), &mut ctx);
        stack.undo(&mut ctx);
        assert!(stack.can_redo());
        stack.push(Add::standalone(10), &mut ctx); // new edit invalidates redo
        assert!(!stack.can_redo());
        assert_eq!(ctx, 10);
    }

    #[test]
    fn consecutive_mergeable_commands_share_one_entry() {
        let mut ctx: Ctx = 0;
        let mut stack = CommandStack::new();
        // Simulate a drag: many small mergeable adds.
        for _ in 0..5 {
            stack.push(Add::new(2), &mut ctx);
        }
        assert_eq!(ctx, 10);
        assert_eq!(stack.undo_depth(), 1, "drag should be one undo entry");
        // A single undo reverses the whole drag.
        stack.undo(&mut ctx);
        assert_eq!(ctx, 0);
    }

    #[test]
    fn group_is_atomic() {
        let mut ctx: Ctx = 0;
        let mut stack = CommandStack::new();
        let mut group = Group::new("Duplicate+Offset");
        group.push(Add::standalone(4));
        group.push(Add::standalone(6));
        stack.push_boxed(Box::new(group), &mut ctx);
        assert_eq!(ctx, 10);
        assert_eq!(stack.undo_depth(), 1);
        assert_eq!(stack.undo(&mut ctx).as_deref(), Some("Duplicate+Offset"));
        assert_eq!(ctx, 0, "group undoes as one atomic step");
    }

    #[test]
    fn capacity_drops_oldest_entries() {
        let mut ctx: Ctx = 0;
        let mut stack = CommandStack::with_capacity(3);
        for i in 1..=5 {
            stack.push(Add::standalone(i), &mut ctx);
        }
        // Only the last 3 entries are retained for undo.
        assert_eq!(stack.undo_depth(), 3);
        // Undoing all retained entries removes 3+4+5 = 12 from the final 15.
        while stack.undo(&mut ctx).is_some() {}
        assert_eq!(ctx, 1 + 2); // the dropped 1 and 2 can't be undone
    }
}
