using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; namespace Hawkeye.VisionBuilder.Features.ImagesStrip; public class ImagesViewModel { private const int DefaultBufferSize = 10; private string? _directoryPath; private List _allImagePaths = new(); private int _currentIndex = -1; private int _bufferSize; private HashSet _currentLoadedIndices = new(); public ObservableCollection LoadedImages { get; } = new(); public int BufferSize { get => _bufferSize; set { if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "Buffer size must be non-negative."); _bufferSize = value; if (_currentIndex >= 0) LoadWindow(); } } public int CurrentIndex => _currentIndex; public int CurrentRelativeIndex => _currentIndex - (_currentIndex - _bufferSize < 0 ? 0 : _currentIndex - _bufferSize); public int CurrentIndexOnLoaded => GetIndexOfLoaded(); public int TotalCount => _allImagePaths.Count; public string? CurrentImagePath => _currentIndex >= 0 && _currentIndex < _allImagePaths.Count ? _allImagePaths[_currentIndex] : null; public bool CanGoNext => _currentIndex < _allImagePaths.Count - 1; public bool CanGoPrevious => _currentIndex > 0; public bool HasImages => _allImagePaths.Count > 0; // Events public event EventHandler? ImagesAdded; public event EventHandler? ImagesRemoved; public event EventHandler? ImagesCleared; public event EventHandler? CurrentImageChanged; public event EventHandler? ImagesLoaded; public event EventHandler? WindowRefreshed; public ImagesViewModel() : this(DefaultBufferSize) { } public ImagesViewModel(int bufferSize) { if (bufferSize < 0) throw new ArgumentOutOfRangeException(nameof(bufferSize), "Buffer size must be non-negative."); _bufferSize = bufferSize; } private int GetIndexOfLoaded() { return LoadedImages.FirstOrDefault(img => img.IsCurrent)?.Index ?? -1; } public int GetIndexFromPath(string path) { return _allImagePaths.IndexOf(path); } /// /// Loads all image files from the specified directory path. /// /// The directory path containing images /// Optional search pattern (default: *.* for all files) /// Optional list of supported extensions (e.g., .jpg, .png, .bmp) public void LoadFromDirectory(string directoryPath, string searchPattern = "*.*", string[]? supportedExtensions = null) { if (string.IsNullOrWhiteSpace(directoryPath)) throw new ArgumentException("Directory path cannot be null or empty.", nameof(directoryPath)); if (!Directory.Exists(directoryPath)) throw new DirectoryNotFoundException($"Directory not found: {directoryPath}"); _directoryPath = directoryPath; var defaultExtensions = new[] { ".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tif", ".tiff" }; var extensions = supportedExtensions ?? defaultExtensions; var files = Directory.GetFiles(directoryPath, searchPattern, SearchOption.AllDirectories) .Where(f => !Path.GetFileNameWithoutExtension(f).Contains("_analysis") && extensions.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase)) .OrderBy(f => f) .ToList(); _allImagePaths = files; _currentIndex = files.Count > 0 ? 0 : -1; LoadWindow(); OnImagesLoaded(new ImagesLoadedEventArgs(files.Count, _directoryPath)); OnCurrentImageChanged(new CurrentImageChangedEventArgs(CurrentIndex,CurrentIndex,CurrentImagePath)); } /// /// Navigate to the first image. /// public bool First() { if (_allImagePaths.Count == 0) return false; if (_currentIndex == 0) return false; var oldIndex = _currentIndex; _currentIndex = 0; LoadWindow(); OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath)); return true; } /// /// Navigate to the previous image. /// public bool Previous() { if (!CanGoPrevious) return false; var oldIndex = _currentIndex; _currentIndex--; LoadWindow(); OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath)); return true; } /// /// Navigate to the next image. /// public bool Next() { if (!CanGoNext) return false; var oldIndex = _currentIndex; _currentIndex++; LoadWindow(); OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath)); return true; } /// /// Navigate to the last image. /// public bool Last() { if (_allImagePaths.Count == 0) return false; var lastIndex = _allImagePaths.Count - 1; if (_currentIndex == lastIndex) return false; var oldIndex = _currentIndex; _currentIndex = lastIndex; LoadWindow(); OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath)); return true; } /// /// Navigate to the Nth image (0-based index). /// public bool SelectNth(int index) { if (index < 0 || index >= _allImagePaths.Count) throw new ArgumentOutOfRangeException(nameof(index), $"Index must be between 0 and {_allImagePaths.Count - 1}."); var oldIndex = _currentIndex; _currentIndex = index; LoadWindow(); OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath)); return true; } public bool SelectByPath(string path) { var index = _allImagePaths.IndexOf(path); if (index < 0) return false; return SelectNth(index); } /// /// Clears all loaded images and resets the state. /// public void Clear() { // Fire remove events for all currently loaded images OnImagesRemoved(_currentLoadedIndices.OrderByDescending(i => i).Select(x => new ImageRemovedEventArgs(x, _allImagePaths[x])).ToArray()); _allImagePaths.Clear(); LoadedImages.Clear(); _currentLoadedIndices.Clear(); _currentIndex = -1; _directoryPath = null; OnImagesCleared(); } /// /// Refreshes the current window by reloading images from disk. /// public void RefreshWindow() { LoadWindow(); OnWindowRefreshed(); } /// /// Reloads the directory, useful when files have been added or removed. /// public void ReloadDirectory(bool preserveCurrentImage = true) { if (string.IsNullOrWhiteSpace(_directoryPath)) return; string? currentPath = preserveCurrentImage ? CurrentImagePath : null; LoadFromDirectory(_directoryPath); if (preserveCurrentImage && currentPath != null) { var newIndex = _allImagePaths.IndexOf(currentPath); if (newIndex >= 0) { _currentIndex = newIndex; LoadWindow(); } } } private void LoadWindow() { if (_currentIndex < 0 || _allImagePaths.Count == 0) { OnImagesRemoved(_currentLoadedIndices.OrderByDescending(i => i).Select(x => new ImageRemovedEventArgs(x, _allImagePaths[x])).ToArray()); LoadedImages.Clear(); _currentLoadedIndices.Clear(); return; } int startIndex = Math.Max(0, _currentIndex - _bufferSize); int endIndex = Math.Min(_allImagePaths.Count - 1, _currentIndex + _bufferSize); // Create a set of new indices that should be loaded var newIndices = new HashSet(); for (int i = startIndex; i <= endIndex; i++) { newIndices.Add(i); } // Find indices to remove (in current but not in new) var indicesToRemove = _currentLoadedIndices.Except(newIndices).OrderByDescending(i => i).ToList(); // Find indices to add (in new but not in current) var indicesToAdd = newIndices.Except(_currentLoadedIndices).OrderBy(i => i).ToList(); // Remove images that are no longer in the window foreach (var index in indicesToRemove) { var item = LoadedImages.FirstOrDefault(img => img.Index == index); if (item != null) { LoadedImages.Remove(item); } _currentLoadedIndices.Remove(index); } OnImagesRemoved(indicesToRemove.Select(x=>new ImageRemovedEventArgs(x, _allImagePaths[x])).ToArray()); // Add new images to the window foreach (var index in indicesToAdd) { var item = new ImageItem(index, _allImagePaths[index], index == _currentIndex); // Insert at the correct position to maintain order int insertIndex = 0; for (int i = 0; i < LoadedImages.Count; i++) { if (LoadedImages[i].Index < index) insertIndex = i + 1; else break; } LoadedImages.Insert(insertIndex, item); _currentLoadedIndices.Add(index); } OnImagesAdded(indicesToAdd.Select(x => new ImageAddedEventArgs(x, _allImagePaths[x])).ToArray()); // Update IsCurrent flag for all items for (int i = 0; i < LoadedImages.Count; i++) { var currentItem = LoadedImages[i]; if (currentItem.IsCurrent != (currentItem.Index == _currentIndex)) { // Replace with updated item LoadedImages[i] = new ImageItem(currentItem.Index, currentItem.FilePath, currentItem.Index == _currentIndex); } } } protected virtual void OnImagesAdded(ImageAddedEventArgs[] e) { ImagesAdded?.Invoke(this, e); } protected virtual void OnImagesRemoved(ImageRemovedEventArgs[] e) { ImagesRemoved?.Invoke(this, e); } protected virtual void OnImagesCleared() { ImagesCleared?.Invoke(this, EventArgs.Empty); } protected virtual void OnCurrentImageChanged(CurrentImageChangedEventArgs e) { CurrentImageChanged?.Invoke(this, e); } protected virtual void OnImagesLoaded(ImagesLoadedEventArgs e) { ImagesLoaded?.Invoke(this, e); } protected virtual void OnWindowRefreshed() { WindowRefreshed?.Invoke(this, EventArgs.Empty); } } public class ImageItem { public int Index { get; } public string FilePath { get; } public string FileName { get; } public bool IsCurrent { get; } public ImageItem(int index, string filePath, bool isCurrent) { Index = index; FilePath = filePath; FileName = Path.GetFileName(filePath); IsCurrent = isCurrent; } } public class ImageAddedEventArgs : EventArgs { public int Index { get; } public string FilePath { get; } public ImageAddedEventArgs(int index, string filePath) { Index = index; FilePath = filePath; } } public class ImageRemovedEventArgs : EventArgs { public int Index { get; } public string FilePath { get; } public ImageRemovedEventArgs(int index, string filePath) { Index = index; FilePath = filePath; } } public class CurrentImageChangedEventArgs : EventArgs { public int OldIndex { get; } public int NewIndex { get; } public string? NewImagePath { get; } public CurrentImageChangedEventArgs(int oldIndex, int newIndex, string? newImagePath) { OldIndex = oldIndex; NewIndex = newIndex; NewImagePath = newImagePath; } } public class ImagesLoadedEventArgs : EventArgs { public int TotalCount { get; } public string DirectoryPath { get; } public ImagesLoadedEventArgs(int totalCount, string directoryPath) { TotalCount = totalCount; DirectoryPath = directoryPath; } }