using Common; using Common.Helpers; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MultiTerm.Core.Types; using MultiTerm.Protocols.Model; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text; namespace MultiTerm.Core.ViewModel; public partial class MultiFormatDataViewModel : ObservableObject { private readonly IContext uiContext; private int dataCharacterCount = 0; private List? listOfPreviousCharacters = null; #region Observable Properties [ObservableProperty] private ObservableCollection data = new(); [ObservableProperty] private string dataAsString = string.Empty; [ObservableProperty] private ObservableCollection? selected = new(); [ObservableProperty] NewlineSeparatorType newlineSeparator = NewlineSeparatorType.None; [ObservableProperty] private string selectedDataFirstAbsoluteTime = string.Empty; [ObservableProperty] private string selectedDataTimediff = string.Empty; #endregion /// /// Newline Sequence string inserted on every newline in the . /// public static readonly string NewlineSequence = Environment.NewLine; public MultiFormatDataViewModel(IContext context) { this.uiContext = context; } public void HandleNewData(IEnumerable newRawData) { // update collection and string, invoke ui thread if necessary ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate { this.DataAsString = InsertNewNewCharactersIntoCollection(this.Data, this.DataAsString, ref this.dataCharacterCount, ref this.listOfPreviousCharacters, this.NewlineSeparator, newRawData); }); } /// /// After changing the newline separator, the data needs to be reordered. /// partial void OnNewlineSeparatorChanged(NewlineSeparatorType value) { // update collection, invoke ui thread if necessary ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate { var reorderedCollection = ReorderCollection(this.Data, value); this.Data = reorderedCollection.Item1; this.DataAsString = reorderedCollection.Item2; }); } [RelayCommand] public void Clear() { // update collection and string, invoke ui thread if necessary ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate { this.DataAsString = string.Empty; this.Data.Clear(); }); } partial void OnSelectedChanging(ObservableCollection? value) { // remove event handler when there was previously a collection of selected if(this.Selected != null) { this.Selected.CollectionChanged -= this.Selected_CollectionChanged; } } partial void OnSelectedChanged(ObservableCollection? value) { // add event handler if (this.Selected != null) { this.Selected.CollectionChanged += Selected_CollectionChanged; } // manually update this.UpdateTimeProperties(); } private void Selected_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { this.UpdateTimeProperties(); } private void UpdateTimeProperties() { if (this.Selected == null || this.Selected.Count == 0) { this.SelectedDataFirstAbsoluteTime = string.Empty; this.SelectedDataTimediff = string.Empty; return; } if (this.Selected.Count == 1) { this.SelectedDataFirstAbsoluteTime = $"Time: {this.Selected.First().Time:HH:mm:ss.ff}"; this.SelectedDataTimediff = string.Empty; } if (this.Selected.Count > 1) { TimeSpan timediff; TimeOnly lastSelected = this.Selected.Last().Time, firstSelected = this.Selected.First().Time; Debug.WriteLine($"First time selected: {firstSelected:HH.mm:ss.ff} Ticks: {firstSelected.Ticks}"); Debug.WriteLine($"Last time selected: {lastSelected:HH.mm:ss.ff} Ticks: {lastSelected.Ticks}"); // calculate timedifference according to selection direction // (preventive. selection should arrive sorted correctly) if (lastSelected > firstSelected) { timediff = lastSelected - firstSelected; } else { timediff = firstSelected - lastSelected; } Debug.WriteLine($"Calculated timediff Ticks: {timediff.Ticks}"); this.SelectedDataFirstAbsoluteTime = $"Time (first selected): {firstSelected:HH:mm:ss.ff}"; this.SelectedDataTimediff = $"Difference (first to last): {timediff.TotalMilliseconds:n} ms"; } } #region Collection Manipulation /// /// Function that reorders a given collection with item type using the given . /// Reordered collection is returned, but LineIdentifier is also overwritten in the parameter. /// /// items in this collection will be reordered /// separator between lines /// reordered collection and string private static Tuple, string> ReorderCollection(ObservableCollection currentCollection, NewlineSeparatorType newlineSeparatorType) { ObservableCollection newCollection = new(); StringBuilder stringBuilder = new(); // local vars int lineCounter = 0; List? previousBytes = null; // iterate through items foreach (ByteDataViewModel item in currentCollection) { item.LineIdentifier = lineCounter; newCollection.Add(item); stringBuilder.Append(item.DisplayStringChar); switch (ShouldIntroduceNewlineAfterThisByte(item.Byte, previousBytes, newlineSeparatorType)) { case ShouldIntroduceNewlineAfterThisByteResult.NoNewline: // a decision could be made, previousBytes can be cleared if (previousBytes != null) { previousBytes = null; } // nothing to do break; case ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline: // a decision could be made, previousBytes can be cleared if (previousBytes != null) { previousBytes = null; } // increase line count lineCounter++; // append line in string stringBuilder.Append(NewlineSequence); break; case ShouldIntroduceNewlineAfterThisByteResult.RequiresMoreCharacters: // first time that more bytes are required => create new list previousBytes ??= new List(); // add current byte to list previousBytes.Add(item.Byte); break; default: throw new Exception($"'{nameof(ReorderCollection)}()' failed because of error when checking if a newline should be introduced."); } } return Tuple.Create(newCollection, stringBuilder.ToString()); } /// /// Function that handles a collection of new bytes that should end up in the collection . /// In case a new line is required, according to the given , it is automatically introduced. /// Following parameters need to be referenced and stored outside: and . /// /// collection to add the bytes to /// collection as string /// current line count /// list of previous bytes, newest at the last position of the list, null if nothing is stored /// separator between seperate lines /// bytes to add to the /// string representation of data /// in case of any error private static string InsertNewNewCharactersIntoCollection( ObservableCollection dataCollection, string dataCollectionAsString, ref int collectionLineCounter, ref List? previousBytes, NewlineSeparatorType newlineSeparatorType, IEnumerable newBytes) { StringBuilder stringBuilder = new(); stringBuilder.Append(dataCollectionAsString); // go through every byte foreach (ExtendedByte newByte in newBytes) { // add to collection and string with the current counter, invoking UI context if necssary var currentLineCounter = collectionLineCounter; dataCollection.Add(new ByteDataViewModel(newByte, currentLineCounter)); stringBuilder.Append(newByte.ToCharacterString()); switch (ShouldIntroduceNewlineAfterThisByte(newByte.Byte, previousBytes, newlineSeparatorType)) { case ShouldIntroduceNewlineAfterThisByteResult.NoNewline: // a decision could be made, previousBytes can be cleared if (previousBytes != null) { previousBytes = null; } // nothing to do break; case ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline: // a decision could be made, previousBytes can be cleared if (previousBytes != null) { previousBytes = null; } // increase line count collectionLineCounter++; // append line in string stringBuilder.Append(NewlineSequence); break; case ShouldIntroduceNewlineAfterThisByteResult.RequiresMoreCharacters: // first time that more bytes are required => create new list previousBytes ??= new List(); // add current byte to list previousBytes.Add(newByte.Byte); break; default: throw new Exception($"'{nameof(InsertNewNewCharactersIntoCollection)}()' failed because of error when checking if a newline should be introduced."); } } return stringBuilder.ToString(); } /// /// Result type for /// public enum ShouldIntroduceNewlineAfterThisByteResult { /// /// No newline is required. /// NoNewline, /// /// A newline shall be introduced after this byte. /// IntroduceNewline, /// /// Following characters are required to finalize result wether a newline shall be introduced or not. /// RequiresMoreCharacters } /// /// Function to check wether a newline shall be introduced after the given . /// Since some newline sequences will require multiple bytes in correct order, a more complex handling is required, which is possible using this function. /// /// the current dataByte in the collection (or a single byte) /// list of previous bytes, newest at the last position of the list, null if not required /// separator type /// enum result of type /// if the handling for the is not implemented private static ShouldIntroduceNewlineAfterThisByteResult ShouldIntroduceNewlineAfterThisByte(byte dataByte, List? previousBytes, NewlineSeparatorType newlineSeparatorType) { var result = ShouldIntroduceNewlineAfterThisByteResult.NoNewline; switch (newlineSeparatorType) { case NewlineSeparatorType.None: break; case NewlineSeparatorType.CR: if (dataByte == (byte)'\r') { result = ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline; } break; case NewlineSeparatorType.LF: if (dataByte == (byte)'\n') { result = ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline; } break; case NewlineSeparatorType.CR_LF: if (dataByte == (byte)'\r') { result = ShouldIntroduceNewlineAfterThisByteResult.RequiresMoreCharacters; } if (dataByte == (byte)'\n') { if (previousBytes != null && previousBytes.Last() == (byte)'\r') { result = ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline; } } break; default: throw new NotImplementedException($"'{nameof(ShouldIntroduceNewlineAfterThisByte)}()' does not implement handling for {nameof(NewlineSeparatorType)} {newlineSeparatorType}"); } return result; } #endregion }