#nullable enable
using T3.Core.DataTypes.DataSet;
namespace T3.Editor.UiModel.Commands.Animation;
///
/// Undoable removal of either whole s or events inside a time
/// range on selected channels. Backs the "Remove" button in the DataSet output view; the
/// two modes share a command so the user gets a single undo entry regardless of which
/// branch fired. Captures the data needed for restore at construction time;
/// applies the removal and re-inserts in reverse order so indices line
/// up with the original list state.
///
///
/// Event lists may be appended to from a background recorder thread, so every mutation
/// takes the per-channel Events lock. The command captures references to live
/// objects; if the itself is replaced
/// (e.g. file reload) before undo runs, the restore quietly no-ops on the missing channels
/// rather than throwing.
///
internal sealed class RemoveDataSetItemsCommand : ICommand
{
public string Name => _isChannelRemoval ? "Remove DataSet channels" : "Remove DataSet events";
public bool IsUndoable => true;
///
/// Channel-removal mode: drops the listed channels entirely (events and metadata).
/// Caller supplies the channels to remove — usually the user's selection. Undo
/// re-inserts them at their original indices.
///
public static RemoveDataSetItemsCommand ForChannels(DataSet dataSet, IReadOnlyList channels)
{
var entries = new List(channels.Count);
foreach (var channel in channels)
{
var index = dataSet.Channels.IndexOf(channel);
if (index < 0)
continue;
entries.Add(new ChannelEntry(index, channel));
}
// Stable removal order: descending index so each remove doesn't shift the next.
entries.Sort((a, b) => b.OriginalIndex.CompareTo(a.OriginalIndex));
return new RemoveDataSetItemsCommand(dataSet, channelEntries: entries);
}
///
/// Event-range mode: deletes every event whose falls in
/// [min(rangeA, rangeB), max(rangeA, rangeB)] on each supplied channel. If
/// is empty the full list is
/// the implicit selection — matches the "no-channel-selection means all channels"
/// behaviour the Remove button already used.
///
public static RemoveDataSetItemsCommand ForEventRange(DataSet dataSet,
IReadOnlyList channels,
double rangeA,
double rangeB)
{
var rangeMin = Math.Min(rangeA, rangeB);
var rangeMax = Math.Max(rangeA, rangeB);
var effective = channels.Count > 0 ? channels : (IReadOnlyList)dataSet.Channels;
var entries = new List(effective.Count);
foreach (var channel in effective)
{
// Snapshot the indices + values that match the range so Undo can re-insert.
// Walking once here is cheaper than re-scanning Events in Do() — and lets
// Undo restore even if the channel is later cleared by another command.
List? matches = null;
lock (channel.Events)
{
for (var i = 0; i < channel.Events.Count; i++)
{
var ev = channel.Events[i];
if (ev == null)
continue;
if (ev.Time < rangeMin || ev.Time > rangeMax)
continue;
matches ??= new List();
matches.Add(new EventRestore(i, ev));
}
}
if (matches != null)
entries.Add(new EventRangeEntry(channel, matches));
}
return new RemoveDataSetItemsCommand(dataSet, eventRangeEntries: entries);
}
private RemoveDataSetItemsCommand(DataSet dataSet,
List? channelEntries = null,
List? eventRangeEntries = null)
{
_dataSet = dataSet;
_channelEntries = channelEntries;
_eventRangeEntries = eventRangeEntries;
_isChannelRemoval = channelEntries != null;
}
public void Do()
{
if (_channelEntries != null)
{
// Entries are already in descending-index order so each remove leaves the
// surviving indices intact for the next one.
foreach (var entry in _channelEntries)
{
if (entry.OriginalIndex >= 0 && entry.OriginalIndex < _dataSet.Channels.Count
&& ReferenceEquals(_dataSet.Channels[entry.OriginalIndex], entry.Channel))
{
_dataSet.Channels.RemoveAt(entry.OriginalIndex);
}
else
{
// Channel was moved/replaced since capture — fall back to a reference
// remove so we still drop the right object if it's still present.
_dataSet.Channels.Remove(entry.Channel);
}
}
PersistChange();
return;
}
if (_eventRangeEntries != null)
{
foreach (var entry in _eventRangeEntries)
{
lock (entry.Channel.Events)
{
// Iterate the captured restores in descending index order so each
// removal at i doesn't shift the index of the next.
for (var i = entry.Events.Count - 1; i >= 0; i--)
{
var restore = entry.Events[i];
if (restore.OriginalIndex >= 0
&& restore.OriginalIndex < entry.Channel.Events.Count
&& ReferenceEquals(entry.Channel.Events[restore.OriginalIndex], restore.Event))
{
entry.Channel.Events.RemoveAt(restore.OriginalIndex);
}
else
{
// Index has drifted (e.g. concurrent recording). Reference
// remove keeps the contract — the event we promised to remove
// is gone, even if it costs an O(n) scan.
entry.Channel.Events.Remove(restore.Event);
}
}
}
}
PersistChange();
}
}
public void Undo()
{
if (_channelEntries != null)
{
// Insert in ascending order so each insertion lands at the captured index
// without later inserts displacing earlier ones.
for (var i = _channelEntries.Count - 1; i >= 0; i--)
{
var entry = _channelEntries[i];
var insertAt = Math.Min(entry.OriginalIndex, _dataSet.Channels.Count);
_dataSet.Channels.Insert(insertAt, entry.Channel);
}
PersistChange();
return;
}
if (_eventRangeEntries != null)
{
foreach (var entry in _eventRangeEntries)
{
lock (entry.Channel.Events)
{
// Restore in ascending captured-index order so earlier inserts
// don't push later ones out of place.
for (var i = 0; i < entry.Events.Count; i++)
{
var restore = entry.Events[i];
var insertAt = Math.Min(restore.OriginalIndex, entry.Channel.Events.Count);
entry.Channel.Events.Insert(insertAt, restore.Event);
}
}
}
PersistChange();
}
}
///
/// Mirrors the in-memory mutation to the source .data file. If the dataset
/// isn't cache-owned (e.g. an in-flight recording before EndRecording has
/// finalised the file) the edit stays in memory only — logged so the user can spot
/// that the change won't survive a reload, but not surfaced as an error since the
/// in-memory effect still played out.
///
private void PersistChange()
{
if (!DataSetCache.TryWriteBack(_dataSet, out var reason))
{
Log.Warning($"RemoveDataSetItemsCommand: in-memory edit applied but not persisted ({reason})");
}
}
private readonly DataSet _dataSet;
private readonly List? _channelEntries;
private readonly List? _eventRangeEntries;
private readonly bool _isChannelRemoval;
private readonly record struct ChannelEntry(int OriginalIndex, DataChannel Channel);
private readonly record struct EventRestore(int OriginalIndex, DataEvent? Event);
private sealed record EventRangeEntry(DataChannel Channel, List Events);
}