namespace ytLive.Models;
///
/// Undo/redo for what the creator does in the preview: move, resize, reorder, add, remove, hide.
///
/// Snapshots hold element REFERENCES, not copies. The model has a dozen subclasses
/// with dozens of properties each; a deep clone would be a large, fragile mirror of every one of
/// them, and it would silently miss the next field someone adds. Holding the live instances and
/// recording the values the preview can change (geometry, visibility, order, and which elements a
/// scene contains) means a new model property cannot break undo — it simply is not part of this
/// stack's contract.
///
/// Scope, stated plainly: this covers layout edits. Property edits (colour, opacity,
/// border, font) are not captured yet — pressing Ctrl+Z after one of those restores the last
/// layout change instead. That is a limitation, not a bug to discover later.
///
/// Bounded: entries; the oldest falls off. Snapshots are
/// references plus a handful of doubles, so the stack costs kilobytes, not megabytes.
///
public sealed class PreviewHistory
{
public const int MaxDepth = 50;
private readonly List _undo = new();
private readonly List _redo = new();
public bool CanUndo => _undo.Count > 0;
public bool CanRedo => _redo.Count > 0;
public int UndoDepth => _undo.Count;
public int RedoDepth => _redo.Count;
/// Record the layout as it is now, before an edit. Clears the redo branch — a new
/// edit after an undo is a new future.
public void Capture(IEnumerable scenes)
{
_undo.Add(LayoutState.From(scenes));
_redo.Clear();
if (_undo.Count > MaxDepth) _undo.RemoveAt(0);
}
/// Restore the previous layout. The current layout is pushed onto the redo stack, so
/// redo is exact even after several undos.
public bool Undo(IEnumerable scenes)
{
if (_undo.Count == 0) return false;
var sceneList = scenes as IList ?? scenes.ToList();
_redo.Add(LayoutState.From(sceneList));
var previous = _undo[^1];
_undo.RemoveAt(_undo.Count - 1);
previous.RestoreTo(sceneList);
return true;
}
public bool Redo(IEnumerable scenes)
{
if (_redo.Count == 0) return false;
var sceneList = scenes as IList ?? scenes.ToList();
_undo.Add(LayoutState.From(sceneList));
var next = _redo[^1];
_redo.RemoveAt(_redo.Count - 1);
next.RestoreTo(sceneList);
return true;
}
/// Dropped when the layout is reloaded from disk — undoing across a reload would
/// resurrect a layout the creator never had.
public void Clear()
{
_undo.Clear();
_redo.Clear();
}
/// One scene's contents and geometry at a point in time.
private sealed record SceneState(Scene Scene, SceneElement[] Order, ElementState[] Elements);
private sealed record ElementState(SceneElement Element, double X, double Y, double Width, double Height, bool IsVisible);
private sealed record LayoutState(SceneState[] Scenes)
{
public static LayoutState From(IEnumerable scenes) => new(
scenes.Select(scene => new SceneState(
scene,
scene.Elements.ToArray(),
scene.Elements.Select(element => new ElementState(
element, element.X, element.Y, element.Width, element.Height, element.IsVisible)).ToArray()))
.ToArray());
public void RestoreTo(IList scenes)
{
foreach (var state in Scenes)
{
// A scene removed from the layout since the snapshot is not restored — the
// layout, not the history, owns which scenes exist.
if (!scenes.Contains(state.Scene)) continue;
foreach (var element in state.Elements)
{
element.Element.X = element.X;
element.Element.Y = element.Y;
element.Element.Width = element.Width;
element.Element.Height = element.Height;
element.Element.IsVisible = element.IsVisible;
}
var elements = state.Scene.Elements;
// Membership AND order: this is what makes add / remove / reorder undo-able.
if (elements.SequenceEqual(state.Order)) continue;
elements.Clear();
foreach (var element in state.Order) elements.Add(element);
}
}
}
}