Files @ 6a6817b17a06
Branch filter:

Location: ATITD-Tools/Desert-Paint-Codex/ViewModels/SimulatorViewModel.cs

Jason Maltzen
Simulator view updates: new warning when the recipe is below minimum concentration. Add the missing reactions to the warning about missing reactions. Show the current saved recipe for a color, and allow replacing/saving the current simulated recipe as the recipe for that color.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Input.Platform;
using DesertPaintCodex.Models;
using DesertPaintCodex.Services;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;

namespace DesertPaintCodex.ViewModels
{
    public class SimulatorViewModel : ViewModelBase
    {
        private RecipeLibraryViewModel? _recipeLibraryVM;

        private ViewModelBase? _recipeLibraryActivity;
        public ViewModelBase? RecipeLibraryActivity { get => _recipeLibraryActivity; private set => this.RaiseAndSetIfChanged(ref _recipeLibraryActivity, value); }


        private PaintColor? _paintColor;
        public PaintColor? PaintColor
        {
            get => _paintColor;
            set => this.RaiseAndSetIfChanged(ref _paintColor, value);
        }

        // Stealing the recipe view from the  paint generator
        private GeneratorRecipe? _existingRecipe;
        public GeneratorRecipe? ExistingRecipe { get => _existingRecipe; private set => this.RaiseAndSetIfChanged(ref _existingRecipe, value); }

        private bool _hasMissingReactions;
        public bool HasMissingReactions
        {
            get => _hasMissingReactions;
            set => this.RaiseAndSetIfChanged(ref _hasMissingReactions, value);
        }

        private bool _isGoodRecipe;

        public bool IsGoodRecipe
        {
            get => _isGoodRecipe;
            set => this.RaiseAndSetIfChanged(ref _isGoodRecipe, value);
        }

        private bool _isValidConcentration;
        public bool IsValidConcentration
        {
            get => _isValidConcentration;
            set => this.RaiseAndSetIfChanged(ref _isValidConcentration, value);
        }

        private string _missingReactionList = string.Empty;
        public string MissingReactionList
        {
            get => _missingReactionList;
            set => this.RaiseAndSetIfChanged(ref _missingReactionList, value);
        }

        public ObservableCollection<Reagent> Reagents { get; } = new();
        public ObservableCollection<Reagent> ActiveReagents { get; } = new();
        public ObservableCollection<RecipeItem> RecipeItems { get; } = new();

        
        private readonly PaintRecipe _currentRecipe = new();
        private PaintColor _tempColor = new PaintColor(0, 0,0);

        public SimulatorViewModel()
        {
            List<string> reagentNames = ReagentService.Names;
            for (int i = 0; i < reagentNames.Count; i++)
            {
                Reagents.Add(ReagentService.GetReagent(reagentNames[i]));
            }

            ActiveReagents.CollectionChanged += OnActiveReagentsChanged;

            RecipeItems
                .ToObservableChangeSet()
                .AutoRefresh(item => item.Quantity)
                .Subscribe(_ => Refresh());
        }

        public async void CopyToClipboard()
        {
            IClipboard clipboard = Application.Current.Clipboard;
            await clipboard.SetTextAsync(_currentRecipe.ToString());
        }

        public void MoveItemUp(RecipeItem item)
        {
            int pos = RecipeItems.IndexOf(item);
            if (pos <= 0) return;

            RecipeItems.RemoveAt(pos);
            RecipeItems.Insert(pos - 1, item);

            Refresh();
        }

        public void MoveItemDown(RecipeItem item)
        {
            int pos = RecipeItems.IndexOf(item);
            if ((pos < 0) || (pos >= RecipeItems.Count - 1)) return;

            RecipeItems.RemoveAt(pos);
            RecipeItems.Insert(pos + 1, item);

            Refresh();
        }

        private void ShowRecipeLibrary()
        {
            _recipeLibraryVM = new RecipeLibraryViewModel();
            Observable.Merge(_recipeLibraryVM.Ok, _recipeLibraryVM.Cancel).Subscribe(_ => LoadRecipe());

            RecipeLibraryActivity = _recipeLibraryVM;
        }


        // Show the list of recipes and allow the user to pick one to load
        public void LoadRecipe()
        {
            // TODO: show the list of recipes
        }

        public void SelectRecipeToLoad(PaintRecipe recipe)
        {
            // 1: Clear the current recipe
            _currentRecipe.Clear();
            RecipeItems.Clear();

            // 2: Set the recipe items from the loaded recipe
            foreach (PaintRecipe.ReagentQuantity reagentQuantity in recipe.Reagents)
            {
                Reagent reagent = ReagentService.GetReagent(reagentQuantity.Name);
                if (reagent != null)
                {
                    RecipeItems.Add(new RecipeItem(reagent, reagentQuantity.Quantity));
                }
            }
            UpdateRecipe();
        }

        // Replace the recipe for a color with the current recipe
        public void ReplaceRecipe()
        {
            if (IsGoodRecipe && IsValidConcentration)
            {
                if (ProfileManager.CurrentProfile == null) return;

                string colorName = PaletteService.FindNearest(_currentRecipe.ReactedColor);
                _tempColor.Set(colorName, _currentRecipe.ReactedColor);
                
                ProfileManager.CurrentProfile.Recipes[colorName].CopyFrom(_currentRecipe);

                UpdateExistingRecipe();
            }
        }

        private void UpdateExistingRecipe()
        {
            if (IsGoodRecipe)
            {
                string colorName = PaletteService.FindNearest(_currentRecipe.ReactedColor);
                if ((ProfileManager.CurrentProfile != null) && (ProfileManager.CurrentProfile.Recipes.TryGetValue(colorName, out PaintRecipe? paintRecipe)))
                {
                    System.Diagnostics.Debug.WriteLine($"Setting existing recipe for {colorName}.");

                    GeneratorRecipe? existingRecipe = ExistingRecipe;
                    if (existingRecipe == null)
                    {
                        _tempColor.Set(colorName, _currentRecipe.ReactedColor);
                        existingRecipe = new GeneratorRecipe(_tempColor);
                    }
                    else
                    {
                        existingRecipe.Color.Set(colorName, _currentRecipe.ReactedColor);
                    }
                    existingRecipe.DraftRecipe(paintRecipe);
                    ExistingRecipe = null;
                    ExistingRecipe = existingRecipe;
                }
                else
                {
                    ExistingRecipe = null;
                }
            }
            else
            {
                ExistingRecipe = null;
            }
        }

        private void UpdateRecipe()
        {
            _currentRecipe.Clear();
            foreach (RecipeItem entry in RecipeItems)
            {
                if (!entry.Unused)
                {
                    _currentRecipe.AddReagent(entry.Reagent.Name, entry.Quantity);
                }
            }

            PaintColor = null; // TODO: Find a better way to kick the paint swatch when reassigning color from the same ref.
            PaintColor          = _currentRecipe.ReactedColor;
            HasMissingReactions = _currentRecipe.HasMissingReactions();
            if (HasMissingReactions)
            {
                List<(string, string)> missingReactions = _currentRecipe.MissingReactionPairs();
                List<string> missingReactionsStrings = new List<string>(missingReactions.Count);
                foreach ((string reagent1, string reagent2) in missingReactions)
                {
                    missingReactionsStrings.Add($"{reagent1}+{reagent2}");
                }
                MissingReactionList = string.Join(", ", missingReactionsStrings);
            }
            IsGoodRecipe        = !HasMissingReactions && _currentRecipe.IsValidForConcentration(10);
            IsValidConcentration = _currentRecipe.IsValidForConcentration(10);

            UpdateExistingRecipe();
        }

        private void OnActiveReagentsChanged(object? sender, NotifyCollectionChangedEventArgs e)
        {
            HashSet<Reagent> unmatchedReagents = new(ActiveReagents);
            
            for (int i = RecipeItems.Count - 1; i >= 0; i--)
            {
                bool found = false;
                foreach (Reagent reagent in ActiveReagents)
                {
                    if (reagent == RecipeItems[i].Reagent)
                    {
                        unmatchedReagents.Remove(reagent);
                        found = true;
                        break;
                    }
                }

                if (!found) RecipeItems.RemoveAt(i);
            }

            foreach (Reagent reagent in unmatchedReagents)
            {
                RecipeItems.Add(new RecipeItem(reagent, 1));
            }

            Refresh();
        }

        private void Refresh()
        {
            UpdateFlags();
            UpdateRecipe();
        }

        private void UpdateFlags()
        {
            int activeIngredients = 0;
            for (int i = 0; i < RecipeItems.Count; i++)
            {
                RecipeItem item = RecipeItems[i];
                item.First = i == 0;
                item.Last = i == RecipeItems.Count - 1;
                if (item.Quantity > 0)
                {
                    activeIngredients++;
                }
                item.Unused = ((activeIngredients > 5) && item.Reagent.IsCatalyst) || (item.Quantity == 0);
            }
        }
    }
}