using System; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Tasks; using DesertPaintCodex.Services; using JetBrains.Annotations; namespace DesertPaintCodex.Models { public class ReactionTest : INotifyPropertyChanged, IProgress, IComparable { public enum TestState { Unset = -1, Untested, Scanning, LabNotFound, ClippedResult, GoodResult, Saved } public Reagent Reagent1 { get; } public Reagent Reagent2 { get; } private Reagent? _bufferReagentFirst; public Reagent? BufferReagentFirst { get => _bufferReagentFirst; set { if (_bufferReagentFirst == value) return; _bufferReagentFirst = value; UpdateRecipe(); NotifyPropertyChanged(nameof(BufferReagentFirst)); NotifyPropertyChanged(nameof(CanScan)); } } private Reagent? _bufferReagentLast; public Reagent? BufferReagentLast { get => _bufferReagentLast; set { if (_bufferReagentLast == value) return; _bufferReagentLast = value; UpdateRecipe(); NotifyPropertyChanged(nameof(BufferReagentLast)); NotifyPropertyChanged(nameof(CanScan)); } } private bool _useFirstBuffer; public bool UseFirstBuffer { get => _useFirstBuffer; set { _useFirstBuffer = value; UpdateRecipe(); NotifyPropertyChanged(nameof(UseFirstBuffer)); NotifyPropertyChanged(nameof(CanScan)); NotifyPropertyChanged(nameof(ShowFirstBuffer)); NotifyPropertyChanged(nameof(ShowLastBuffer)); } } private ClipType _clipType; public ClipType Clipped { get => _clipType; set { _clipType = value; NotifyPropertyChanged(nameof(Clipped)); } } public bool IsAllCatalysts { get; } private Reaction? _reaction; public Reaction? Reaction { get => _reaction; set { _reaction = value; NotifyPropertyChanged(nameof(Reaction)); } } private Reaction? _badReaction; public Reaction? BadReaction { get => _badReaction; set { _badReaction = value; NotifyPropertyChanged(nameof(BadReaction)); } } private int _scanProgress; public int ScanProgress { get => _scanProgress; set { _scanProgress = value; NotifyPropertyChanged(nameof(ScanProgress)); } } private TestState _state; public TestState State { get => _state; set { _state = value; NotifyPropertyChanged(nameof(State)); NotifyPropertyChanged(nameof(Requires3Way)); NotifyPropertyChanged(nameof(CanScan)); NotifyPropertyChanged(nameof(IsScanning)); NotifyPropertyChanged(nameof(HasResults)); NotifyPropertyChanged(nameof(HasReaction)); NotifyPropertyChanged(nameof(CanClear)); NotifyPropertyChanged(nameof(CanSave)); NotifyPropertyChanged(nameof(NoLab)); NotifyPropertyChanged(nameof(CanPickBuffer)); NotifyPropertyChanged(nameof(ShowFirstBuffer)); NotifyPropertyChanged(nameof(ShowLastBuffer)); } } public bool Requires3Way => (State == TestState.ClippedResult) || IsAllCatalysts; public bool CanScan => (State is TestState.Untested or TestState.LabNotFound) || ((State == TestState.ClippedResult) && BufferIsSelected); public bool IsScanning => State == TestState.Scanning; public bool HasResults => (ObservedColor != null) && (State is TestState.ClippedResult or TestState.GoodResult or TestState.LabNotFound); public bool HasReaction => State is TestState.ClippedResult or TestState.GoodResult or TestState.Saved; public bool CanClear => State is TestState.ClippedResult or TestState.GoodResult or TestState.Saved; public bool CanSave => State == TestState.GoodResult; public bool NoLab => State == TestState.LabNotFound; public bool CanPickBuffer => (State == TestState.ClippedResult) || IsAllCatalysts; public bool ShowFirstBuffer => Requires3Way && UseFirstBuffer; public bool ShowLastBuffer => Requires3Way && !UseFirstBuffer; private PaintColor? _hypotheticalColor; public PaintColor? HypotheticalColor { get => _hypotheticalColor; set { _hypotheticalColor = value; NotifyPropertyChanged(nameof(HypotheticalColor)); } } private PaintColor? _observedColor; public PaintColor? ObservedColor { get => _observedColor; set { _observedColor = value; NotifyPropertyChanged(nameof(ObservedColor)); } } private readonly PaintRecipe _recipe = new(); public bool IsStub { get; } public ReactionTest(Reagent reagent1, Reagent reagent2, Reaction? reaction, ClipType clipType, bool isStub = false) { Reagent1 = reagent1; Reagent2 = reagent2; UseFirstBuffer = true; IsAllCatalysts = reagent1.IsCatalyst && reagent2.IsCatalyst; Clipped = clipType; Reaction = reaction; State = (reaction != null) ? TestState.Saved : (clipType == ClipType.None) ? TestState.Untested : TestState.ClippedResult; _recipe.AddReagent(reagent1.Name); _recipe.AddReagent(reagent2.Name); IsStub = isStub; UpdateHypotheticalColor(); } #region Actions public async Task StartScan() { Clipped = ClipType.None; ScanProgress = 0; Reaction = null; BadReaction = null; State = TestState.Scanning; bool testing3Way = Requires3Way && BufferIsSelected; bool foundLab = await ReactionScannerService.Instance.CaptureReactionAsync(this); if (foundLab) { ObservedColor = ReactionScannerService.Instance.RecordedColor; if (_observedColor != null) { Clipped = _observedColor.Red switch { 0 => ClipType.RedLow, 255 => ClipType.RedHigh, _ => ClipType.None } | _observedColor.Green switch { 0 => ClipType.GreenLow, 255 => ClipType.GreenHigh, _ => ClipType.None } | _observedColor.Blue switch { 0 => ClipType.BlueLow, 255 => ClipType.BlueHigh, _ => ClipType.None }; if (Clipped == ClipType.None) { State = TestState.GoodResult; Reaction = CalculateReaction(); } else { Reaction? reaction = CalculateReaction(); bool extrapolated = false; // SPECIAL CASE: // Check to see if we've got a white-shift reaction that's partially clipped, where we can // still extrapolate the reaction, based on the available information. bool isWhiteShift = (reaction != null) && Clipped switch { ClipType.RedLow => (reaction.Blue == reaction.Green) && (reaction.Blue != 0), ClipType.RedHigh => (reaction.Blue == reaction.Green) && (reaction.Blue != 0), ClipType.BlueLow => (reaction.Red == reaction.Green) && (reaction.Red != 0), ClipType.BlueHigh => (reaction.Red == reaction.Green) && (reaction.Red != 0), ClipType.GreenLow => (reaction.Red == reaction.Blue) && (reaction.Red != 0), ClipType.GreenHigh => (reaction.Red == reaction.Blue) && (reaction.Red != 0), ClipType.RedLow|ClipType.BlueLow => (reaction.Green < 0), ClipType.RedLow|ClipType.GreenLow => (reaction.Blue < 0), ClipType.BlueLow|ClipType.GreenLow => (reaction.Red < 0), ClipType.RedHigh | ClipType.BlueHigh => (reaction.Green > 0), ClipType.RedHigh | ClipType.GreenHigh => (reaction.Blue > 0), ClipType.BlueHigh | ClipType.GreenHigh => (reaction.Red > 0), _ => false }; if (!testing3Way && (reaction != null) && isWhiteShift) { PaintColor baseColor = _recipe.BaseColor; if ((reaction.Red < 0) && (reaction.Green < 0) && (reaction.Blue < 0)) { // White-shift down clip. extrapolated = ExtrapolateWhiteFromOneChannel(_observedColor.Red, 0, reaction.Red); if (!extrapolated) extrapolated = ExtrapolateWhiteFromOneChannel(_observedColor.Green, 0, reaction.Green); if (!extrapolated) extrapolated = ExtrapolateWhiteFromOneChannel(_observedColor.Blue, 0, reaction.Blue); } else if ((reaction.Red > 0) && (reaction.Green > 0) && (reaction.Blue > 0)) { // White-shift up clip. extrapolated = ExtrapolateWhiteFromOneChannel(_observedColor.Red, 255, reaction.Red); if (!extrapolated) extrapolated = ExtrapolateWhiteFromOneChannel(_observedColor.Green, 255, reaction.Green); if (!extrapolated) extrapolated = ExtrapolateWhiteFromOneChannel(_observedColor.Blue, 255, reaction.Blue); } } if (!extrapolated) { State = TestState.ClippedResult; BadReaction = CalculateReaction(); } } PlayerProfile? profile = ProfileManager.CurrentProfile; profile?.SetPairClipStatus(Reagent1, Reagent2, Clipped); } } else { Debug.WriteLine("ERROR: Lab UI not found."); State = TestState.LabNotFound; } } private bool ExtrapolateWhiteFromOneChannel(int result, int clipBound, int reaction) { if (result == clipBound) return false; Clipped = ClipType.None; State = TestState.GoodResult; Reaction = new Reaction(reaction, reaction, reaction); return true; } public void CancelScan() { ReactionScannerService.Instance.CancelScan(); State = TestState.Untested; } public void MarkInert() { Reaction = new Reaction(0, 0, 0); State = TestState.GoodResult; SaveReaction(); } public void ClearReaction() { PlayerProfile? profile = ProfileManager.CurrentProfile; if (profile == null) return; profile.Reactions.Remove(Reagent1, Reagent2); profile.SetPairClipStatus(Reagent1, Reagent2, ClipType.None); if (State == TestState.Saved) { profile.Save(); } BufferReagentFirst = null; BufferReagentLast = null; Reaction = null; BadReaction = null; Clipped = ClipType.None; State = TestState.Untested; ObservedColor = null; // Clear it out UpdateRecipe(); } public void SaveReaction() { PlayerProfile? profile = ProfileManager.CurrentProfile; if (profile == null) return; profile.Reactions.Set(Reagent1, Reagent2, Reaction); profile.Save(); State = TestState.Saved; } #endregion #region Internals private Reaction? CalculateReaction() { if (ProfileManager.CurrentProfile == null) return null; if (HypotheticalColor == null) return null; if (ObservedColor == null) return null; if (_useFirstBuffer) { if (BufferReagentFirst != null) { return ReactionScannerService.Calculate3WayReaction(ProfileManager.CurrentProfile, HypotheticalColor, ObservedColor, BufferReagentFirst, Reagent1, Reagent2, true); } } else { if (BufferReagentLast != null) { return ReactionScannerService.Calculate3WayReaction(ProfileManager.CurrentProfile, HypotheticalColor, ObservedColor, Reagent1, Reagent2, BufferReagentLast, false); } } return ReactionScannerService.CalculateReaction(HypotheticalColor, ObservedColor); } private void UpdateHypotheticalColor() { HypotheticalColor = null; HypotheticalColor = (IsAllCatalysts && !BufferIsSelected) ? null : _recipe.BaseColor; } private bool BufferIsSelected => UseFirstBuffer ? (BufferReagentFirst != null) : (BufferReagentLast != null); private void UpdateRecipe() { _recipe.Clear(); if (_useFirstBuffer) { if (_bufferReagentFirst == null) { _recipe.AddReagent(Reagent1.Name); _recipe.AddReagent(Reagent2.Name); } else { _recipe.AddReagent(_bufferReagentFirst.Name); _recipe.AddReagent(Reagent1.Name); _recipe.AddReagent(Reagent2.Name); } } else { if (_bufferReagentLast == null) { _recipe.AddReagent(Reagent1.Name); _recipe.AddReagent(Reagent2.Name); } else { _recipe.AddReagent(Reagent1.Name); _recipe.AddReagent(Reagent2.Name); _recipe.AddReagent(_bufferReagentLast.Name); } } UpdateHypotheticalColor(); } #endregion #region Interface Implementations public event PropertyChangedEventHandler? PropertyChanged; [NotifyPropertyChangedInvocator] private void NotifyPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public void Report(float value) { ScanProgress = (int)Math.Round(value * 100); } #endregion public int CompareTo(ReactionTest? other) { if (other == null) return 1; if (Clipped == ClipType.None) { if (other.Clipped != ClipType.None) return -1; } else if (other.Clipped == ClipType.None) return 1; if (IsAllCatalysts) { if (!other.IsAllCatalysts) return 1; } else if (other.IsAllCatalysts) return -1; return string.CompareOrdinal(Reagent1.Name, other.Reagent1.Name) switch { < 0 => -1, > 0 => 1, _ => string.CompareOrdinal(Reagent2.Name, other.Reagent2.Name) }; } } }