using System.Collections.Generic; using System.Linq; using UnityEngine; namespace OM.TimelineCreator.Runtime { /// /// Manages a collection of OM_ClipBase-derived clips for an IOM_TimelinePlayer. /// Handles initialization, retrieval, adding, removing, and manipulation of clips within the timeline. /// Uses [SerializeReference] to correctly handle polymorphic lists of clip types. /// /// The specific type of OM_ClipBase being managed (e.g., AnimoraClip). [System.Serializable] public class OM_ClipsManager where T : OM_ClipBase { /// /// The list containing all the clips managed by this instance. /// Marked with [SerializeReference] to support storing different subclasses of T. /// [SerializeReference] protected List clips; /// /// Flag indicating whether the manager has been initialized (e.g., null checks performed). /// Prevents redundant initialization work. /// protected bool IsInitialized; /// /// Initializes the clips manager specifically for editor use. /// Ensures the clips list exists and removes any null entries that might occur due to serialization issues or manual deletion. /// Re-calculates the OrderIndex for all remaining clips to ensure consistency. /// public virtual void InitForEditor() { clips ??= new List(); // Ensure the list is not null var hasChanged = false; // Iterate backwards to safely remove null entries for (var i = clips.Count - 1; i >= 0; i--) { if (clips[i] == null) { clips.RemoveAt(i); hasChanged = true; // Mark that the list was modified } } // If null entries were removed, re-index the remaining clips if (!hasChanged) return; // No need to re-index if no changes were made for (var i = 0; i < clips.Count; i++) { if (clips[i] != null) // Double-check for null just in case { clips[i].OrderIndex = i; // Assign sequential order index } } } /// /// Initializes the clips manager for runtime use. /// Ensures the clips list exists and removes null entries. /// Sets the IsInitialized flag to prevent re-initialization. /// public virtual void Init() { if (IsInitialized) return; // Prevent multiple initializations IsInitialized = true; clips ??= new List(); // Ensure the list is not null // Iterate backwards to safely remove null entries (less critical at runtime unless expecting external modification) for (var i = clips.Count - 1; i >= 0; i--) { if (clips[i] == null) { clips.RemoveAt(i); // Consider logging a warning here if null clips are unexpected at runtime } } // Note: Runtime Init doesn't typically re-index like InitForEditor } /// /// Gets the complete, mutable list of all clips managed by this instance. /// /// The List containing the clips. public List GetClips() { clips ??= new List(); // Ensure list exists before returning return clips; } /// /// Gets an enumerable collection of clips that are currently considered playable (based on their CanBePlayed() method). /// /// An IEnumerable containing only the playable clips. public IEnumerable GetCanBePlayedClips() { clips ??= new List(); // Use LINQ Where to filter based on the clip's CanBePlayed status return clips.Where(clip => clip != null && clip.CanBePlayed()); // Added null check } /// /// Gets an enumerable collection of clips sorted by their OrderIndex property. /// /// An IEnumerable containing the clips sorted by OrderIndex. public IEnumerable GetClipsBasedOnOrderIndex() { clips ??= new List(); // Use LINQ OrderBy to sort based on the clip's OrderIndex return clips.Where(clip => clip != null).OrderBy(clip => clip.OrderIndex); // Added null check } /// /// Adds a new clip to the end of the managed list and sets its OrderIndex. /// Records an undo operation in the editor context. /// /// The clip instance to add. /// The timeline player instance (used for recording undo). public virtual void AddClip(T clip, IOM_TimelinePlayer target) { if (clip == null) { Debug.LogError("Attempted to add a null clip."); return; } clips ??= new List(); target.RecordUndo("Add Clip"); // Record the action for editor undo clips.Add(clip); // Assign the order index based on the new list count (0-based index) clip.OrderIndex = clips.Count - 1; // Consider calling target.OnValidate() or similar if needed after adding } /// /// Removes a specified clip from the managed list and adjusts the OrderIndex of subsequent clips. /// Records an undo operation in the editor context. /// /// The clip instance to remove. /// The timeline player instance (used for recording undo). public virtual void RemoveClip(T clipToRemove, IOM_TimelinePlayer target) { if (clipToRemove == null || clips == null) return; // Nothing to remove or list doesn't exist target.RecordUndo("Remove Clip"); // Record the action for editor undo int removedIndex = clipToRemove.OrderIndex; // Store the index before removing if (clips.Remove(clipToRemove)) // Attempt to remove the clip { // Adjust the order index of the clips that came after the removed one foreach (var clip in clips) { if (clip != null && clip.OrderIndex > removedIndex) { clip.OrderIndex--; // Decrement index to fill the gap } } // Consider calling target.OnValidate() or similar if needed after removing } else { Debug.LogWarning("Attempted to remove a clip that was not found in the manager."); } } /// /// Inserts a clip at a specific index in the list, adjusting the OrderIndex of existing clips. /// Records an undo operation. /// /// The clip to insert. /// The timeline player instance (for recording undo). /// The target index where the clip should be inserted. /// True if insertion was successful, false otherwise (e.g., null clip, invalid index). public virtual bool InsertClip(T clipToInsert, IOM_TimelinePlayer target, int index) { // --- Input Validation --- if (clipToInsert == null) { Debug.LogError("Trying to insert a null clip to the clips list"); return false; } clips ??= new List(); // Ensure list exists // Allow inserting at the end (index == Count) if (index < 0 || index > clips.Count) { Debug.LogError($"Trying to insert a clip at an invalid index ({index}). Valid range is [0..{clips.Count}]."); return false; } // --- Logic --- target.RecordUndo("Insert Clip"); // Record action // Increment OrderIndex for existing clips at or after the target index foreach (var clip in clips) { if (clip != null && clip.OrderIndex >= index) { clip.OrderIndex++; } } // Set the new clip's index and add it clipToInsert.OrderIndex = index; // Use List.Insert to place it at the correct position if maintaining list order matters, // otherwise just Add and rely on OrderIndex property for sorting later. Add is simpler. clips.Add(clipToInsert); // Optional: Re-sort the list immediately based on OrderIndex if needed // clips = clips.OrderBy(c => c.OrderIndex).ToList(); // Consider calling target.OnValidate() or similar if needed after inserting return true; } /// /// Duplicates a specified clip and inserts the copy immediately after the original. /// Records an undo operation. Uses JsonUtility for deep copying the clip data. /// /// The clip instance to duplicate. /// The timeline player instance (used for recording undo). public virtual void DuplicateClip(T clipToDuplicate, IOM_TimelinePlayer target) { if (clipToDuplicate == null || clips == null) { Debug.LogError("Attempted to duplicate a null clip or manager has no clip list."); return; } int index = clipToDuplicate.OrderIndex; // Find the actual index in the list, OrderIndex might not match if list wasn't recently sorted/validated // int listIndex = clips.IndexOf(clipToDuplicate); // if (listIndex == -1) { ... handle error ... } var type = clipToDuplicate.GetType(); // Get the specific derived type // Use JsonUtility to create a deep copy of the clip's serialized data var json = JsonUtility.ToJson(clipToDuplicate); var newClip = (T)JsonUtility.FromJson(json, type); if (newClip == null) { Debug.LogError($"Failed to duplicate clip '{clipToDuplicate.ClipName}' using JsonUtility."); return; } // Use InsertClip to add the duplicate at the next index // RecordUndo is handled within InsertClip InsertClip(newClip, target, index + 1); } /// /// Replaces the current list of clips with a new list. /// Primarily used for loading saved data or replacing the entire timeline content. /// /// The new list of clips to manage. public void PopulateWithClips(List newClips) { // Directly replace the list. Assumes newClips is valid. // Consider adding validation or re-indexing if necessary. this.clips = newClips ?? new List(); // Ensure it's not set to null IsInitialized = false; // Mark as uninitialized so Init() can run again if needed // Consider calling InitForEditor() or Init() immediately after if required. } } }