using System.Collections;
using System.Diagnostics.CodeAnalysis;
namespace DodoSSH.Client.Domain;
///
/// The port forwards saved on a host, in the order the user added them.
///
///
///
/// A dedicated type rather than a list, for the reason gives: a plain list on a
/// record compares by reference, so every host would read as changed on every sync pass.
///
///
/// Ordered and unique by id. Order is how the session sidebar lists them. Uniqueness is by
/// rather than by value, keeping the first occurrence, because the id is
/// what the merge keys on — two entries with one id would be one forward the merge could not tell apart.
///
///
/// Invalid rules are not rejected at construction, for the reason every collection in this assembly gives:
/// they arrive from another client's payload, and a throw here would fail the whole sync pass.
/// catches them.
///
///
public sealed class PortForwardList : IReadOnlyList, IEquatable
{
private readonly PortForwardRule[] rules;
private readonly int hash;
private PortForwardList(PortForwardRule[] rules)
{
this.rules = rules;
hash = ComputeHash(rules);
}
/// No forwards.
public static PortForwardList Empty { get; } = new([]);
///
public int Count => rules.Length;
///
public PortForwardRule this[int index] => rules[index];
/// Copies a sequence of rules, keeping order and the first rule with any given id.
public static PortForwardList Create(IEnumerable rules)
{
ArgumentNullException.ThrowIfNull(rules);
var kept = new List();
var seen = new HashSet();
foreach (var rule in rules)
{
if (rule is null)
{
throw new ArgumentException("A forward list cannot contain a null rule.", nameof(rules));
}
if (seen.Add(rule.Id))
{
kept.Add(rule);
}
}
return kept.Count == 0 ? Empty : new PortForwardList([.. kept]);
}
///
/// The list as a map from id to rule, in list order, which is the shape
/// takes.
///
///
/// Built in list order for the reason is: the merge unions keys
/// ancestor first, then local, then remote, so an ordered map is what keeps "existing forwards, then
/// additions" without the merge ordering anything itself. Unlike a pinned path, a value here can change
/// under its key, so a conflict is reachable: both machines editing the same forward differently.
///
public IReadOnlyDictionary ToIdMap()
{
var map = new Dictionary(rules.Length);
foreach (var rule in rules)
{
map[rule.Id] = rule;
}
return map;
}
///
public bool Equals(PortForwardList? other)
{
if (ReferenceEquals(this, other))
{
return true;
}
return other is not null
&& other.hash == hash
&& rules.AsSpan().SequenceEqual(other.rules);
}
///
public override bool Equals(object? obj) => Equals(obj as PortForwardList);
///
public override int GetHashCode() => hash;
///
public IEnumerator GetEnumerator() => ((IEnumerable)rules).GetEnumerator();
///
IEnumerator IEnumerable.GetEnumerator() => rules.GetEnumerator();
/// Contents equality, tolerating nulls on either side.
[SuppressMessage(
"Usage",
"CA2225:Operator overloads have named alternates",
Justification = "Equals(PortForwardList) is the named alternate.")]
public static bool operator ==(PortForwardList? left, PortForwardList? right) =>
left is null ? right is null : left.Equals(right);
/// Contents inequality.
public static bool operator !=(PortForwardList? left, PortForwardList? right) => !(left == right);
private static int ComputeHash(PortForwardRule[] rules)
{
// Order-sensitive, because order is kept.
var accumulator = new HashCode();
accumulator.Add(rules.Length);
foreach (var rule in rules)
{
accumulator.Add(rule);
}
return accumulator.ToHashCode();
}
}