using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
/// A set of changes which can be applied to a mod data field.
public class ChangeDescriptor
{
/*********
** Accessors
*********/
/// The values to add to the field.
public ISet Add { get; }
/// The values to remove from the field.
public ISet Remove { get; }
/// The values to replace in the field, if matched.
public IReadOnlyDictionary Replace { get; }
/// Whether the change descriptor would make any changes.
public bool HasChanges { get; }
/// Format a raw value into a normalized form.
public Func FormatValue { get; }
/*********
** Public methods
*********/
/// Construct an instance.
/// The values to add to the field.
/// The values to remove from the field.
/// The values to replace in the field, if matched.
/// Format a raw value into a normalized form.
public ChangeDescriptor(ISet add, ISet remove, IReadOnlyDictionary replace, Func formatValue)
{
this.Add = add;
this.Remove = remove;
this.Replace = replace;
this.HasChanges = add.Any() || remove.Any() || replace.Any();
this.FormatValue = formatValue;
}
/// Apply the change descriptors to a comma-delimited field.
/// The raw field text.
/// Returns the modified field.
#if NET5_0_OR_GREATER
[return: NotNullIfNotNull("rawField")]
#endif
public string? ApplyToCopy(string? rawField)
{
// get list
List values = !string.IsNullOrWhiteSpace(rawField)
? new List(
from field in rawField.Split(',')
let value = field.Trim()
where value.Length > 0
select value
)
: new List();
// apply changes
this.Apply(values);
// format
if (rawField == null && !values.Any())
return null;
return string.Join(", ", values);
}
/// Apply the change descriptors to the given field values.
/// The field values.
/// Returns the modified field values.
public void Apply(List values)
{
// replace/remove values
if (this.Replace.Any() || this.Remove.Any())
{
for (int i = values.Count - 1; i >= 0; i--)
{
string value = this.FormatValue(values[i].Trim());
if (this.Remove.Contains(value))
values.RemoveAt(i);
else if (this.Replace.TryGetValue(value, out string? newValue))
values[i] = newValue;
}
}
// add values
if (this.Add.Any())
{
HashSet curValues = new HashSet(values.Select(p => p.Trim()), StringComparer.OrdinalIgnoreCase);
foreach (string add in this.Add)
{
if (!curValues.Contains(add))
{
values.Add(add);
curValues.Add(add);
}
}
}
}
///
public override string ToString()
{
if (!this.HasChanges)
return string.Empty;
List descriptors = new List(this.Add.Count + this.Remove.Count + this.Replace.Count);
foreach (string add in this.Add)
descriptors.Add($"+{add}");
foreach (string remove in this.Remove)
descriptors.Add($"-{remove}");
foreach (var pair in this.Replace)
descriptors.Add($"{pair.Key} → {pair.Value}");
return string.Join(", ", descriptors);
}
/// Parse a raw change descriptor string into a model.
/// The raw change descriptor.
/// The human-readable error message describing any invalid values that were ignored.
/// Format a raw value into a normalized form if needed.
public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Func? formatValue = null)
{
// init
formatValue ??= p => p;
var add = new HashSet(StringComparer.OrdinalIgnoreCase);
var remove = new HashSet(StringComparer.OrdinalIgnoreCase);
var replace = new Dictionary(StringComparer.OrdinalIgnoreCase);
// parse each change in the descriptor
if (!string.IsNullOrWhiteSpace(descriptor))
{
List rawErrors = new List();
foreach (string rawEntry in descriptor.Split(','))
{
// normalize entry
string entry = rawEntry.Trim();
if (entry == string.Empty)
continue;
// parse as replace (old value → new value)
if (entry.Contains('→'))
{
string[] parts = entry.Split(new[] { '→' }, 2);
string oldValue = formatValue(parts[0].Trim());
string newValue = formatValue(parts[1].Trim());
if (oldValue == string.Empty)
{
rawErrors.Add($"Failed parsing '{rawEntry}': can't map from a blank old value. Use the '+value' format to add a value.");
continue;
}
if (newValue == string.Empty)
{
rawErrors.Add($"Failed parsing '{rawEntry}': can't map to a blank value. Use the '-value' format to remove a value.");
continue;
}
replace[oldValue] = newValue;
}
// else as remove
else if (entry.StartsWith("-"))
{
entry = formatValue(entry.Substring(1).Trim());
remove.Add(entry);
}
// else as add
else
{
if (entry.StartsWith("+"))
entry = formatValue(entry.Substring(1).Trim());
add.Add(entry);
}
}
errors = rawErrors.ToArray();
}
else
errors = Array.Empty();
// build model
return new ChangeDescriptor(
add: add,
remove: remove,
replace: new ReadOnlyDictionary(replace),
formatValue: formatValue
);
}
}
}