#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using IFSEngine.Model;
using WpfDisplay.Services;
using WpfDisplay.Helper;
using WpfDisplay.Models;
namespace WpfDisplay.ViewModels;
/// <summary>
/// Viewmodel for the palette browser page.
/// Manages collections, palettes, favorites, sorting, filtering, grouping, and zoom.
/// </summary>
public partial class PaletteBrowserViewModel : ObservableObject
{
private readonly PaletteLibraryService _library;
private readonly MainViewModel _mainVm;
private readonly Workspace _workspace;
public PaletteLibraryService Library => _library;
public MainViewModel MainVm => _mainVm;
private HashSet<Guid> _favorites = [];
[ObservableProperty]
public partial ObservableCollection<PaletteCollectionViewModel> Collections { get; set; } = [];
[ObservableProperty]
public partial ObservableCollection<ColorPaletteViewModel> FavoritePalettes { get; set; } = [];
[ObservableProperty]
public partial ColorPaletteViewModel? SelectedPalette { get; set; }
[ObservableProperty]
public partial PaletteSortMode SortMode { get; set; }
[ObservableProperty]
public partial bool GroupByCollection { get; set; } = true;
[ObservableProperty]
public partial string SearchText { get; set; } = "";
[ObservableProperty]
public partial double Zoom { get; set; } = 200;
/// <summary>Available sort modes for the UI.</summary>
public static PaletteSortMode[] AllSortModes { get; } = (PaletteSortMode[])Enum.GetValues(typeof(PaletteSortMode));
[ObservableProperty]
public partial ImageSource? FractalPreviewImage { get; set; }
// Flat list (used when not grouping)
private ObservableCollection<ColorPaletteViewModel> _allPalettes = [];
public ObservableCollection<ColorPaletteViewModel> AllPalettes => _allPalettes;
[ObservableProperty]
public partial ObservableCollection<PaletteCollectionViewModel> FilteredCollections { get; set; } = [];
[ObservableProperty]
public partial ObservableCollection<ColorPaletteViewModel> FilteredAllPalettes { get; set; } = [];
[ObservableProperty]
public partial ObservableCollection<ColorPaletteViewModel> FilteredFavorites { get; set; } = [];
public bool HasCollections
{
get => Collections.Count > 0;
}
public bool HasFavorites
{
get => FavoritePalettes.Count > 0;
}
public bool HasFilteredFavorites
{
get => FilteredFavorites.Count > 0;
}
// Preview palette gradient stops
public GradientStopCollection? PreviewGradientStops
{
get => SelectedPalette?.GradientStops;
}
public Color? PreviewBackgroundColor
{
get => SelectedPalette?.BackgroundColor;
}
public PaletteBrowserViewModel(PaletteLibraryService library, MainViewModel mainVm)
{
_library = library;
_mainVm = mainVm;
_workspace = mainVm.workspace;
}
public async Task LoadAsync()
{
var collections = await _library.LoadCollectionsAsync();
_favorites = await _library.GetFavoritesAsync();
BuildViewModels(collections);
}
public async Task ReloadAsync()
{
var collections = await _library.ReloadCollectionsAsync();
BuildViewModels(collections);
}
private void BuildViewModels(List<PaletteCollection> collections)
{
// Build flat palette list
var allPaletteVms = collections
.SelectMany(c => c.Palettes.Select(p => new ColorPaletteViewModel(p)))
.ToList();
// Set favorite state
foreach (var vm in allPaletteVms)
vm.IsFavorite = _favorites.Contains(vm.Palette.Id);
// Sort
allPaletteVms = SortPalettes(allPaletteVms).ToList();
// Update collections
Collections.Clear();
foreach (var collection in collections)
{
var cvm = new PaletteCollectionViewModel(collection);
var collectionPalettes = allPaletteVms.Where(p => collection.Palettes.Any(orig => orig.Id == p.Palette.Id)).ToList();
cvm.RefreshPalettes(collectionPalettes);
Collections.Add(cvm);
}
// Update favorites
FavoritePalettes.Clear();
foreach (var vm in allPaletteVms.Where(p => p.IsFavorite))
FavoritePalettes.Add(vm);
// Update flat list
_allPalettes.Clear();
foreach (var vm in allPaletteVms)
_allPalettes.Add(vm);
OnPropertyChanged(nameof(HasCollections));
OnPropertyChanged(nameof(HasFavorites));
ApplyFilter();
}
private void ApplyFilter()
{
string search = SearchText.ToLowerInvariant().Trim();
// Filter all palettes
IEnumerable<ColorPaletteViewModel> filtered = _allPalettes;
if (!string.IsNullOrEmpty(search))
filtered = _allPalettes.Where(p =>
p.Palette.Name.ToLowerInvariant().Contains(search) ||
p.Palette.Tags.Any(t => t.ToLowerInvariant().Contains(search))
);
// Update filtered flat list
FilteredAllPalettes.Clear();
foreach (var vm in filtered)
FilteredAllPalettes.Add(vm);
// Update filtered collections (only show collections with matching palettes)
FilteredCollections.Clear();
foreach (var cvm in Collections)
{
var matchingPalettes = filtered.Where(p =>
cvm.Palettes.Any(orig => orig.Palette.Id == p.Palette.Id)
).ToList();
if (matchingPalettes.Any() || string.IsNullOrEmpty(search))
{
var clone = new PaletteCollectionViewModel(cvm.Collection)
{
IsExpanded = cvm.IsExpanded
};
clone.RefreshPalettes(matchingPalettes);
FilteredCollections.Add(clone);
}
}
// Update filtered favorites
FilteredFavorites.Clear();
foreach (var vm in FavoritePalettes)
{
if (string.IsNullOrEmpty(search) ||
vm.Palette.Name.ToLowerInvariant().Contains(search) ||
vm.Palette.Tags.Any(t => t.ToLowerInvariant().Contains(search)))
{
FilteredFavorites.Add(vm);
}
}
OnPropertyChanged(nameof(HasFavorites));
OnPropertyChanged(nameof(HasFilteredFavorites));
}
private IEnumerable<ColorPaletteViewModel> SortPalettes(IEnumerable<ColorPaletteViewModel> palettes)
{
return SortMode switch
{
PaletteSortMode.Name => palettes.OrderBy(p => p.Palette.Name),
PaletteSortMode.DominantHue => palettes.OrderBy(p => p.DominantHue),
PaletteSortMode.Warmth => palettes.OrderByDescending(p => p.Warmth),
PaletteSortMode.Brightness => palettes.OrderBy(p => p.Brightness),
PaletteSortMode.Saturation => palettes.OrderBy(p => p.Saturation),
PaletteSortMode.KeyCount => palettes.OrderBy(p => p.KeyCount),
_ => palettes
};
}
[RelayCommand]
private void SelectPalette(ColorPaletteViewModel palette)
{
SelectedPalette = palette;
UpdateFractalPreviewAsync();
}
private async void UpdateFractalPreviewAsync()
{
// Fractal preview is deferred to Phase 2.5 / Phase 5 integration
// For now, just invalidate the palette buffer so the main render updates
if (SelectedPalette != null)
{
_workspace.Renderer.InvalidatePaletteBuffer();
}
}
[RelayCommand]
private async Task ToggleFavorite(ColorPaletteViewModel palette)
{
palette.IsFavorite = !palette.IsFavorite;
await _library.SetFavoriteAsync(palette.Palette.Id, palette.IsFavorite);
if (palette.IsFavorite)
FavoritePalettes.Add(palette);
else
FavoritePalettes.Remove(palette);
OnPropertyChanged(nameof(HasFavorites));
ApplyFilter();
}
[RelayCommand]
private async Task CreateCollection()
{
var collection = await _library.AddCollectionAsync("New Collection");
await ReloadAsync();
}
[RelayCommand]
private async Task CreatePalette()
{
if (Collections.Count == 0)
{
MessageBox.Show("Create a collection first.", "No Collections", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
// Create a default palette in the first collection
var targetCollection = Collections[0].Collection;
var palette = new ColorPalette
{
Name = "New Palette",
BackgroundColor = null,
InterpolationMode = InterpolationMode.LinearRGB,
Author = Author.Unknown,
};
palette.KeyColors[0.0] = new System.Numerics.Vector4(1, 1, 1, 1);
palette.KeyColors[1.0] = new System.Numerics.Vector4(1, 1, 1, 1);
await _library.AddPaletteAsync(targetCollection.Id, palette);
await ReloadAsync();
}
[RelayCommand]
private async Task DeleteCollection(PaletteCollectionViewModel collectionVm)
{
var result = MessageBox.Show(
$"Delete collection \"{collectionVm.Collection.Name}\" and all its palettes?",
"Delete Collection",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
{
await _library.RemoveCollectionAsync(collectionVm.Collection.Id);
SelectedPalette = null;
await ReloadAsync();
}
}
[RelayCommand]
private async Task RenameCollection(PaletteCollectionViewModel collectionVm)
{
var newName = Microsoft.VisualBasic.Interaction.InputBox(
"Enter new name:", "Rename Collection", collectionVm.Collection.Name);
if (!string.IsNullOrWhiteSpace(newName) && newName != collectionVm.Collection.Name)
{
collectionVm.Collection.Name = newName;
await _library.SaveCollectionAsync(collectionVm.Collection);
}
}
[RelayCommand]
private async Task EditCollectionDescription(PaletteCollectionViewModel collectionVm)
{
var newDesc = Microsoft.VisualBasic.Interaction.InputBox(
"Enter description:", "Edit Description", collectionVm.Collection.Description);
if (newDesc != collectionVm.Collection.Description)
{
collectionVm.Collection.Description = newDesc;
await _library.SaveCollectionAsync(collectionVm.Collection);
}
}
[RelayCommand]
private async Task ExportCollection(PaletteCollectionViewModel collectionVm)
{
if (collectionVm.PaletteCount == 0)
{
MessageBox.Show("Cannot export an empty collection.", "Export Error", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (DialogHelper.ShowExportGradientDialog(collectionVm.Collection.Name, out string path))
{
try
{
await _library.ExportCollectionAsync(collectionVm.Collection.Id, path);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to export: {ex.Message}", "Export Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
[RelayCommand]
private async Task ImportFlameFile()
{
if (DialogHelper.ShowOpenGradientDialog(out string path))
{
try
{
var collection = await _library.ImportFlameFileAsync(path);
if (collection.Palettes.Count == 0)
{
MessageBox.Show("No palettes found in the file.", "Import Error", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
await ReloadAsync();
}
catch (Exception ex)
{
MessageBox.Show($"Failed to import: {ex.Message}", "Import Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
[RelayCommand]
private async Task DuplicatePalette(ColorPaletteViewModel paletteVm)
{
if (SelectedPalette == null) return;
// Find the collection this palette belongs to
Guid? collectionId = null;
foreach (var cvm in Collections)
{
if (cvm.Palettes.Any(p => p.Palette.Id == paletteVm.Palette.Id))
{
collectionId = cvm.Collection.Id;
break;
}
}
if (collectionId == null) return;
var original = paletteVm.Palette;
var copy = new ColorPalette
{
Name = original.Name + " (Copy)",
BackgroundColor = original.BackgroundColor,
InterpolationMode = original.InterpolationMode,
Author = original.Author,
};
foreach (var kvp in original.KeyColors)
copy.KeyColors[kvp.Key] = kvp.Value;
copy.Tags = new System.Collections.Generic.List<string>(original.Tags);
await _library.AddPaletteAsync(collectionId.Value, copy);
await ReloadAsync();
}
[RelayCommand]
private async Task DeletePalette(ColorPaletteViewModel paletteVm)
{
var result = MessageBox.Show(
$"Delete palette \"{paletteVm.Palette.Name}\"?",
"Delete Palette",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
{
await _library.RemovePaletteAsync(paletteVm.Palette.Id);
if (SelectedPalette?.Palette.Id == paletteVm.Palette.Id)
SelectedPalette = null;
await ReloadAsync();
}
}
[RelayCommand]
private async Task CopyPaletteTo((ColorPaletteViewModel palette, PaletteCollectionViewModel target) args)
{
var paletteVm = args.palette;
var targetCollectionVm = args.target;
var copy = new ColorPalette
{
Name = paletteVm.Palette.Name + " (Copy)",
BackgroundColor = paletteVm.Palette.BackgroundColor,
InterpolationMode = paletteVm.Palette.InterpolationMode,
Author = paletteVm.Palette.Author,
};
foreach (var kvp in paletteVm.Palette.KeyColors)
copy.KeyColors[kvp.Key] = kvp.Value;
copy.Tags = new System.Collections.Generic.List<string>(paletteVm.Palette.Tags);
await _library.AddPaletteAsync(targetCollectionVm.Collection.Id, copy);
await ReloadAsync();
}
[RelayCommand]
private void ApplyToFractal()
{
if (SelectedPalette == null) return;
_workspace.TakeSnapshot();
_workspace.Ifs.Palette = SelectedPalette.Palette;
_workspace.Renderer.InvalidatePaletteBuffer();
}
/// <summary>
/// Open the palette editor for the selected palette.
/// </summary>
[RelayCommand]
public void OpenEditor(ColorPaletteViewModel? paletteVm = null)
{
var target = paletteVm ?? SelectedPalette;
if (target == null) return;
// Find the collection this palette belongs to
PaletteCollection? collection = null;
foreach (var cvm in Collections)
{
if (cvm.Palettes.Any(p => p.Palette.Id == target.Palette.Id))
{
collection = cvm.Collection;
break;
}
}
// Notify the manager to switch to editor page
// This is handled by the window's code-behind via a callback
OnOpenEditor?.Invoke(target.Palette, collection);
}
public event Action<ColorPalette, PaletteCollection?>? OnOpenEditor;
[RelayCommand]
private async Task UpdatePaletteName(string newName)
{
if (SelectedPalette == null) return;
SelectedPalette.Palette.Name = newName;
await SaveSelectedPaletteCollection();
}
private async Task SaveSelectedPaletteCollection()
{
if (SelectedPalette == null) return;
// Find the collection containing this palette
foreach (var cvm in Collections)
{
if (cvm.Palettes.Any(p => p.Palette.Id == SelectedPalette.Palette.Id))
{
await _library.SaveCollectionAsync(cvm.Collection);
return;
}
}
}
partial void OnZoomChanged(double value)
{
// Persist zoom preference
// TODO: Settings.Default.PaletteZoom = value;
}
partial void OnGroupByCollectionChanged(bool value)
{
// Triggered when grouping toggle changes
}
partial void OnSortModeChanged(PaletteSortMode value)
{
RebuildSortedLists();
}
private void RebuildSortedLists()
{
var sorted = SortPalettes(_allPalettes).ToList();
_allPalettes.Clear();
foreach (var vm in sorted)
_allPalettes.Add(vm);
// Rebuild collection palette lists
foreach (var cvm in Collections)
{
var collectionPalettes = sorted.Where(p =>
cvm.Palettes.Any(orig => orig.Palette.Id == p.Palette.Id)
).ToList();
cvm.RefreshPalettes(collectionPalettes);
}
// Rebuild favorites
FavoritePalettes.Clear();
foreach (var vm in _allPalettes.Where(p => p.IsFavorite))
FavoritePalettes.Add(vm);
ApplyFilter();
}
partial void OnSearchTextChanged(string value)
{
ApplyFilter();
}
}