using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Models.Api;
using Unity.Cloud.Collaborate.Models.Structures;
using Unity.Cloud.Collaborate.UserInterface;
using Unity.Cloud.Collaborate.Utilities;
using UnityEngine;
using UnityEngine.Assertions;
namespace Unity.Cloud.Collaborate.Models
{
internal class ChangesModel : IChangesModel
{
protected readonly ISourceControlProvider m_Provider;
///
public event Action UpdatedChangeList;
///
public event Action OnUpdatedSelectedChanges;
///
public event Action BusyStatusUpdated;
///
public event Action StateChanged;
internal Dictionary entryData;
internal Dictionary toggledEntries;
IReadOnlyList m_Conflicted;
readonly ChangeEntryData m_AllItem;
readonly HashSet m_Requests;
const string k_RequestNewList = "request-new-list";
///
public string SavedRevisionSummary { get; set; }
///
public string SavedSearchQuery { get; set; }
///
public int ToggledCount { get; private set; }
///
public int TotalCount { get; private set; }
///
public int ConflictedCount => m_Conflicted.Count;
///
public bool Conflicted => m_Provider.GetConflictedState();
///
public bool Busy => m_Requests.Count != 0;
public ChangesModel(ISourceControlProvider provider)
{
m_Provider = provider;
m_Requests = new HashSet();
m_AllItem = new ChangeEntryData { Entry = new ChangeEntry(string.Empty), All = true };
entryData = new Dictionary();
m_Conflicted = new List();
toggledEntries = new Dictionary();
SavedSearchQuery = string.Empty;
SavedRevisionSummary = string.Empty;
}
///
public void OnStart()
{
// Setup events.
m_Provider.UpdatedChangeList += OnUpdatedChangeList;
m_Provider.UpdatedSelectedChangeList += OnUpdatedSelectedChangesList;
}
///
public void OnStop()
{
// Clean up.
m_Provider.UpdatedChangeList -= OnUpdatedChangeList;
m_Provider.UpdatedSelectedChangeList -= OnUpdatedSelectedChangesList;
}
///
public void RestoreState(IWindowCache cache)
{
// Populate data from cache.
SavedRevisionSummary = cache.RevisionSummary;
SavedSearchQuery = cache.ChangesSearchValue;
toggledEntries = cache.SimpleSelectedItems ?? new Dictionary();
StateChanged?.Invoke();
}
///
public void SaveState(IWindowCache cache)
{
// Save data.
cache.RevisionSummary = SavedRevisionSummary;
cache.ChangesSearchValue = SavedSearchQuery;
cache.SimpleSelectedItems = new SelectedItemsDictionary(toggledEntries);
}
///
/// Event handler for when the source control provider receives an updated history list.
///
void OnUpdatedChangeList()
{
// Only one request at a time.
if (!AddRequest(k_RequestNewList)) return;
m_Provider.RequestChangeList(OnReceivedChangeList);
}
void OnUpdatedSelectedChangesList(IReadOnlyList list)
{
ToggleAllEntries(false);
foreach (var path in list)
{
UpdateEntryToggle(path, true);
}
OnUpdatedSelectedChanges?.Invoke();
}
///
/// Event handler to receive changes from the provider.
///
/// Change list received.
void OnReceivedChangeList([CanBeNull] IReadOnlyList list)
{
if (list != null)
{
UpdateChangeList(list);
UpdatedChangeList?.Invoke();
}
else
{
Debug.LogError("Failed to fetch latest change list.");
}
RemoveRequest(k_RequestNewList);
}
///
/// Convert and cache new list of changes.
///
/// New list of changes.
internal virtual void UpdateChangeList([NotNull] IReadOnlyList list)
{
TotalCount = list.Count;
// Create a new set of containers.
var newEntryData = new Dictionary { [string.Empty] = m_AllItem };
var newToggledEntries = new Dictionary();
var conflicted = new List();
var all = m_AllItem.Toggled;
var toggledCount = 0;
foreach (var entry in list)
{
// Transfer toggled state from old lookup into new.
toggledEntries.TryGetValue(entry.Path, out var toggled);
toggled = toggled || all || entry.Staged;
newToggledEntries[entry.Path] = toggled;
// Create a new data item for the entry.
var item = new ChangeEntryData { Entry = entry, Toggled = toggled };
newEntryData.Add(entry.Path, item);
// Update counts.
if (toggled)
{
toggledCount++;
}
if (entry.Unmerged)
{
conflicted.Add(item);
}
}
// Store the new containers.
entryData = newEntryData;
toggledEntries = newToggledEntries;
ToggledCount = toggledCount;
m_Conflicted = conflicted;
UpdateAllItemToggle();
}
///
public virtual bool UpdateEntryToggle(string path, bool toggled)
{
var entry = (ChangeEntryData)entryData[path];
// Toggle all items if needed.
if (entry.All)
{
return ToggleAllEntries(toggled);
}
// Update the toggled count.
if (entry.Toggled && !toggled)
{
ToggledCount--;
}
else if (!entry.Toggled && toggled)
{
ToggledCount++;
}
// Store the value in the dictionary and data item.
toggledEntries[entry.Entry.Path] = toggled;
entry.Toggled = toggled;
// Update the "All" option if needed.
return UpdateAllItemToggle();
}
///
public IReadOnlyList GetToggledEntries(string query = null)
{
// Filter items by search query
query = StringUtility.TrimAndToLower(query);
return entryData.Values.Where(e => !e.All && e.Toggled && e.Entry.Path.ToLower().Contains(query)).ToList();
}
///
public IReadOnlyList GetUntoggledEntries(string query = null)
{
// Filter items by search query
query = StringUtility.TrimAndToLower(query);
return entryData.Values.Where(e => !e.All && !e.Toggled && e.Entry.Path.ToLower().Contains(query)).ToList();
}
///
public IReadOnlyList GetAllEntries(string query = null)
{
// Filter items by search query
query = StringUtility.TrimAndToLower(query);
return entryData.Values.Where(e => e.Entry.Path.ToLower().Contains(query)).ToList();
}
///
public IReadOnlyList GetConflictedEntries(string query = null)
{
// Filter items by search query
query = StringUtility.TrimAndToLower(query);
return entryData.Values.Where(e => !e.All && e.Conflicted && e.Entry.Path.ToLower().Contains(query))
.ToList();
}
///
/// Update the state of the "All" entry. If all entries are toggled, then "All" should be toggled too;
/// otherwise, "All" should be untoggled.
///
/// True if the "All" entry was modified.
bool UpdateAllItemToggle()
{
// Update state of the "All" option
var allItemToggled = m_AllItem.Toggled;
if (entryData.Count == 0) return false;
if (ToggledCount == entryData.Count - 1)
{
// If every entry is toggled, then set AllItem as toggled.
toggledEntries[m_AllItem.Entry.Path] = true;
m_AllItem.Toggled = true;
return !allItemToggled;
}
// Otherwise, set AllItem as not toggled.
toggledEntries[m_AllItem.Entry.Path] = false;
m_AllItem.Toggled = false;
return allItemToggled;
}
///
/// Toggle on or off all entries in the list.
///
/// Whether to toggle off or on.
/// True if the list has been modified.
bool ToggleAllEntries(bool toggled)
{
// Update all values in the dictionary.
toggledEntries.Keys.ToList().ForEach(x => toggledEntries[x] = toggled);
// Compute the number of toggled items (excluding the single All).
if (toggled)
{
ToggledCount = entryData.Count - 1;
}
else
{
ToggledCount = 0;
}
// Update all values in the list.
foreach (var kv in entryData)
{
((ChangeEntryData)kv.Value).Toggled = toggled;
}
return true;
}
///
/// Add a started request.
///
/// Id of the request to add.
/// False if the request already exists.
bool AddRequest(string requestId)
{
if (m_Requests.Contains(requestId)) return false;
m_Requests.Add(requestId);
// Signal background activity if this is the only thing running.
if (m_Requests.Count == 1)
BusyStatusUpdated?.Invoke(true);
return true;
}
///
/// Remove a finished request.
///
/// Id of the request to remove.
void RemoveRequest(string requestId)
{
Assert.IsTrue(m_Requests.Contains(requestId), $"Expects request to have first been made for it to have been finished: {requestId}");
m_Requests.Remove(requestId);
// Signal no background activity if no requests in progress
if (m_Requests.Count == 0)
BusyStatusUpdated?.Invoke(false);
}
///
public void RequestInitialData()
{
// Only one request at a time.
if (!AddRequest(k_RequestNewList)) return;
m_Provider.RequestChangeList(OnReceivedChangeList);
}
///
public void RequestDiffChanges(string path)
{
m_Provider.RequestDiffChanges(path);
}
///
public void RequestDiscard(IChangeEntry entry)
{
m_Provider.RequestDiscard(entry);
}
///
public void RequestBulkDiscard(IReadOnlyList entries)
{
m_Provider.RequestBulkDiscard(entries);
}
///
public void RequestPublish(string message, IReadOnlyList changes)
{
m_Provider.RequestPublish(message, changes);
}
///
public void RequestShowConflictedDifferences(string path)
{
m_Provider.RequestShowConflictedDifferences(path);
}
///
public void RequestChooseMerge(string path)
{
m_Provider.RequestChooseMerge(path);
}
///
public void RequestChooseMine(string[] paths)
{
m_Provider.RequestChooseMine(paths);
}
///
public void RequestChooseRemote(string[] paths)
{
m_Provider.RequestChooseRemote(paths);
}
///
/// Implementation of IChangeEntryData with each field given a setter so that the data can be updated.
///
class ChangeEntryData : IChangeEntryData
{
///
public IChangeEntry Entry { get; set; }
///
public bool Toggled { get; set; }
///
public bool All { get; set; }
///
public bool ToggleReadOnly => Entry.Staged;
///
public bool Conflicted => Entry.Unmerged;
}
}
}