using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Assertions;
namespace RosettaUI.UndoSystem
{
public static class UndoHistory
{
///
/// IUndoRecordを保持し、Dispose時に指定のActionを呼び出す
/// IUndoRecordにObjectPoolの仕組みは入れたくないが実質ObjectPoolを利用したい
/// UndoRecorderと連携してObjectPool管理するためのもの
///
private readonly struct RecordHolder
{
public IUndoRecord Record { get; }
private readonly Action _onDispose;
public bool IsValid => Record != null;
public RecordHolder(IUndoRecord record, Action onDispose)
{
Record = record;
_onDispose = onDispose;
}
public void Dispose()
{
Record.Dispose();
_onDispose?.Invoke(Record);
}
}
///
/// Undo/Redo用のスタック自体をスタックする
/// 主にModalEditorで一時的にUndo/Redo履歴過去と切り離して管理する場合に利用する
///
private static readonly Stack<(string name, Stack undoStack, Stack redoStack)> HistoryStacks = new();
private static bool _isProcessing;
private static bool _canTryMargeNextRecord = true;
public static IEnumerable HistoryStackNames => HistoryStacks.Select(stack => stack.name);
public static IEnumerable UndoRecords => UndoStack.Select(holder => holder.Record);
public static IEnumerable RedoRecords => RedoStack.Select(holder => holder.Record);
public static bool CanUndo => UndoStack.Count > 0;
public static bool CanRedo => RedoStack.Count > 0;
// Undo/Redo処理中はAddできない
public static bool CanAdd => !_isProcessing;
private static Stack UndoStack => HistoryStacks.Peek().undoStack;
private static Stack RedoStack => HistoryStacks.Peek().redoStack;
static UndoHistory()
{
// 最初の履歴スタックを作成
PushHistoryStack("Default");
}
public static void PushHistoryStack(string name)
{
Assert.IsFalse(_isProcessing, "Cannot push history stack during undo/redo processing");
FixLastUndoRecord();
HistoryStacks.Push((name, new Stack(), new Stack()));
}
public static void PopHistoryStack()
{
Assert.IsFalse(_isProcessing, "Cannot pop history stack during undo/redo processing");
FixLastUndoRecord();
if (HistoryStacks.Count <= 1)
{
return;
}
var (_, undoStack, redoStack) = HistoryStacks.Pop();
ClearStack(undoStack);
ClearStack(redoStack);
}
///
/// UndoStackにrecordを追加する。
///
public static void Add(IUndoRecord record, Action onDispose = null, bool disposeRecordIfNotStacked = true)
{
if (record == null) return;
var holder = new RecordHolder(record, onDispose);
if (_isProcessing)
{
if (disposeRecordIfNotStacked) holder.Dispose();
return;
}
RemoveTopUnavailableRecords(UndoStack);
// マージ可能条件
// - RedoStackが空
// - _canTryMargeNextRecord==true
// - 同じElementでも一度フォーカスが外れたらマージしないようにしたいので外部から_canTryMargeNextRecordで制御できるようにする
if (RedoStack.Count <= 0
&& _canTryMargeNextRecord
&& UndoStack.TryPeek(out var previous)
&& previous.Record.CanMerge(record))
{
previous.Record.Merge(record);
if (disposeRecordIfNotStacked) record.Dispose();
}
else
{
UndoStack.Push(holder);
_canTryMargeNextRecord = true;
}
ClearStack(RedoStack);
}
public static void Clear()
{
ClearStack(UndoStack);
ClearStack(RedoStack);
}
public static void FixLastUndoRecord()
{
_canTryMargeNextRecord = false;
}
public static bool Undo()
{
if (!TryPopAndRemoveUnavailableRecords(UndoStack, out var holder)) return false;
_isProcessing = true;
holder.Record.Undo();
_isProcessing = false;
RedoStack.Push(holder);
return true;
}
public static bool Redo()
{
if (!TryPopAndRemoveUnavailableRecords(RedoStack, out var holder)) return false;
_isProcessing = true;
holder.Record.Redo();
_isProcessing = false;
UndoStack.Push(holder);
return true;
}
private static bool TryPopAndRemoveUnavailableRecords(Stack stack, out RecordHolder holder)
{
RemoveTopUnavailableRecords(stack);
if (stack.TryPop(out holder))
{
return true;
}
holder = default;
return false;
}
private static bool RemoveTopUnavailableRecords(Stack stack)
{
var removed = false;
while (stack.TryPeek(out var holder) && !holder.Record.IsAvailable)
{
stack.Pop().Dispose();
removed = true;
}
return removed;
}
private static void ClearStack(Stack stack)
{
while (stack.TryPop(out var record))
{
record.Dispose();
}
}
}
}