using System.Collections; using Sandbox.MovieMaker; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; namespace Editor.MovieMaker; #nullable enable public static class CollectionExtensions { public static T? FirstOrNull( this IEnumerable enumerable, Func predicate ) where T : struct { foreach ( var item in enumerable ) { if ( predicate( item ) ) { return item; } } return null; } public static IReadOnlyList Slice( this IReadOnlyList list, int offset, int count ) { if ( offset < 0 ) { throw new ArgumentException( "Offset must be >= 0.", nameof( offset ) ); } if ( list.Count < offset + count ) { throw new ArgumentException( "Slice exceeds list element count.", nameof( count ) ); } // Fast paths if ( count == 0 ) return Array.Empty(); if ( offset == 0 && count == list.Count ) return list; switch ( list ) { case T[] array: return new ArraySegment( array, offset, count ); case ImmutableArray immutableArray: return immutableArray.Slice( offset, count ); case ArraySegment segment: return segment.Slice( offset, count ); } // Slow copy return list.Skip( offset ).Take( count ).ToArray(); } /// /// Given two ascending lists of time ranges, union any overlapping pairs between the two lists and return them. /// public static IEnumerable Union( this IEnumerable first, IEnumerable second ) { using var enumeratorA = first.GetEnumerator(); using var enumeratorB = second.GetEnumerator(); var hasItemA = enumeratorA.MoveNext(); var hasItemB = enumeratorB.MoveNext(); while ( hasItemA || hasItemB ) { var next = !hasItemB || hasItemA && enumeratorA.Current.Start <= enumeratorB.Current.Start ? enumeratorA.Current : enumeratorB.Current; while ( true ) { if ( hasItemA && next.Intersect( enumeratorA.Current ) is not null ) { next = next.Union( enumeratorA.Current ); hasItemA = enumeratorA.MoveNext(); continue; } if ( hasItemB && next.Intersect( enumeratorB.Current ) is not null ) { next = next.Union( enumeratorB.Current ); hasItemB = enumeratorB.MoveNext(); continue; } break; } yield return next; } } public static T Pop( this List list ) { var item = list[^1]; list.RemoveAt( list.Count - 1 ); return item; } } public interface ISynchronizedList : IReadOnlyList { public void Clear() => Update( [] ); bool Update( IEnumerable source ); int IndexOf( TSrc src ); } /// /// Maintains a set of , mapped from a collection of . /// public sealed class SynchronizedSet : ISynchronizedList, IHotloadManaged where TSrc : notnull { [SuppressNullKeyWarning] private readonly Dictionary _items; [SuppressNullKeyWarning] private readonly Dictionary _indices; private readonly List> _ordered = new(); private readonly Func _addFunc; private readonly Action? _removeAction; private readonly Func? _updateAction; private bool _keysCanBeInvalid; private bool _keysWereInvalid; public SynchronizedSet( Func addFunc, Action? removeAction = null, Func? updateAction = null, IEqualityComparer? comparer = null ) { _addFunc = addFunc; _removeAction = removeAction; _updateAction = updateAction; _items = new Dictionary( comparer ); _indices = new Dictionary( comparer ); } public TItem? Find( TSrc key ) { RemoveInvalidKeys(); return _items.GetValueOrDefault( key ); } public void Clear() => Update( [] ); public bool Update( IEnumerable source ) { RemoveInvalidKeys(); _indices.Clear(); foreach ( var src in source ) { _indices.Add( src, _indices.Count ); } // We might have removed some items after a hotload, so // that should count as the collection being changed here var changed = _keysWereInvalid; _keysWereInvalid = false; // Remove items for ( var i = _ordered.Count - 1; i >= 0; --i ) { var src = _ordered[i].Key; if ( _indices.ContainsKey( src ) ) continue; _ordered.RemoveAt( i ); if ( !_items.Remove( src, out var item ) ) continue; _removeAction?.Invoke( item ); changed = true; } // Add items foreach ( var src in _indices.Keys ) { if ( _items.ContainsKey( src ) ) continue; var item = _addFunc( src ); _items.Add( src, item ); _ordered.Add( new KeyValuePair( src, item ) ); changed = true; } // Sort and update items _ordered.Sort( ( a, b ) => _indices[a.Key] - _indices[b.Key] ); if ( _updateAction is { } updateAction ) { foreach ( var pair in _ordered ) { changed |= updateAction( pair.Key, pair.Value ); } } return changed; } public int IndexOf( TSrc src ) { RemoveInvalidKeys(); return _indices.TryGetValue( src, out var index ) ? index : -1; } public IEnumerator GetEnumerator() { RemoveInvalidKeys(); return _ordered.Select( x => x.Value ).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { RemoveInvalidKeys(); return _items.GetEnumerator(); } public int Count { get { RemoveInvalidKeys(); return _items.Count; } } public TItem this[ int index ] { get { RemoveInvalidKeys(); return _ordered[index].Value; } } void IHotloadManaged.Persisted() => _keysCanBeInvalid = true; void IHotloadManaged.Created( IReadOnlyDictionary state ) => _keysCanBeInvalid = true; /// /// Called after hotload to remove any items that didn't survive because their types got removed. /// private void RemoveInvalidKeys() { if ( !_keysCanBeInvalid ) return; _keysCanBeInvalid = false; // First pass: remove invalid / null items for ( var i = _ordered.Count - 1; i >= 0; i-- ) { var (src, dst) = _ordered[i]; if ( (TSrc?)src is null ) { // Null items will have been removed from _items dict during hotload, // because dictionaries can't have null keys _removeAction?.Invoke( dst ); _ordered.RemoveAt( i ); _keysWereInvalid = true; } else if ( src is IValid { IsValid: false } ) { _removeAction?.Invoke( dst ); _ordered.RemoveAt( i ); _items.Remove( src ); _keysWereInvalid = true; } } // Second pass: update indices dict _indices.Clear(); for ( var i = 0; i < _ordered.Count; i++ ) { _indices.Add( _ordered[i].Key, i ); } } } /// /// Maintains a list of with the same length as a list of . /// public sealed class SynchronizedList : ISynchronizedList { private readonly List _sources = new(); private readonly List _items = new(); private readonly Func _addFunc; private readonly Action? _removeAction; private readonly UpdateItemDelegate? _updateAction; public IEnumerable Sources => _sources; public delegate bool UpdateItemDelegate( TSrc source, ref TItem item ); public SynchronizedList( Func addFunc, Action? removeAction = null, UpdateItemDelegate? updateAction = null ) { _addFunc = addFunc; _removeAction = removeAction; _updateAction = updateAction; } public void Clear() => Update( [] ); public bool Update( IEnumerable source ) { _sources.Clear(); _sources.AddRange( source ); var changed = false; // Remove items while ( _items.Count > _sources.Count ) { var item = _items.Pop(); _removeAction?.Invoke( item ); changed = true; } // Add items while ( _items.Count < _sources.Count ) { _items.Add( _addFunc( _sources[_items.Count] ) ); changed = true; } if ( _updateAction is { } updateAction ) { for ( var i = 0; i < _sources.Count; ++i ) { var item = _items[i]; if ( updateAction( _sources[i], ref item ) ) { _items[i] = item; changed = true; } } } return changed; } public int IndexOf( TSrc src ) => _sources.IndexOf( src ); public IEnumerator GetEnumerator() => _items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); public int Count => _items.Count; public TItem this[int index] => _items[index]; }