namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
///
/// Turns a device-reported sibling-name sequence (axes, spindles) into unique fixed-tree
/// path segments (Gitea #548).
///
///
///
/// A real CNC can report duplicate sibling names — one observed Makino returns two axis
/// folders both called AA. Before this existed, the emitter offered two identical
/// browse folders, commit-time collision detection refused to author both (same RawPath),
/// and the poll loop's name-keyed snapshot cache let the second axis silently overwrite the
/// first — so the Axes/ subtree was untrustworthy on such a machine.
///
///
/// Scheme: every member of a collision set gets a 1-based occurrence-ordinal suffix in
/// slot order (AA_1, AA_2); unique names stay bare. Three deliberate
/// choices inside that:
///
///
/// Names stay the primary identity, not slot numbers. The segment map is re-derived
/// from the live device at every session bootstrap, so a name-keyed path follows the
/// physical axis across configuration changes; a slot-qualified path (3_AA) would
/// silently rebind to a different physical axis whenever the axis list shifts, and would
/// rename every axis path on every machine for the sake of the rare duplicate.
///
///
/// The whole collision set is suffixed, not just the second occurrence. When two
/// axes are both called AA, the bare segment AA ceases to exist — a bare name
/// that silently means "one of the two" is the same silent-wrongness class #534 and #540
/// were about. A tag authored against bare AA on such a machine reads Bad, never one
/// arbitrary axis's data.
///
///
/// Occurrence ordinal, not absolute slot number. The ordinal is stable under
/// insertion/removal of unrelated axes (a new B axis ahead of the two AAs
/// renames nothing; their slot numbers would both change). If the collision-set membership
/// itself changes — a third AA appears, or one of two is removed — the segments do
/// change; that surfaces on the next browse + commit, and until then the stale authored
/// segment reads Bad rather than another axis's data (the served tree only changes on a
/// config redeploy).
///
///
/// Suffix collisions with genuine names are structurally impossible for axes (FOCAS axis
/// displays are at most two characters — name + suffix — so they cannot contain _),
/// but the generator still checks: a generated segment that would collide with anything
/// else in the set escalates its ordinal until unique, so the output is always
/// collision-free whatever the input.
///
///
internal static class FixedTreeNames
{
///
/// Maps each name in (in slot order) to a unique sibling path
/// segment. Unique names map to themselves; duplicated names map to
/// {name}_{occurrence}. Comparison is case-insensitive, matching the fixed-tree
/// snapshot cache and read-path lookups.
///
/// Device-reported sibling names, in device (slot) order.
/// One segment per input name, unique within the returned list.
public static IReadOnlyList Disambiguate(IEnumerable names)
{
var list = names as IReadOnlyList ?? [.. names];
var counts = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var name in list)
counts[name] = counts.TryGetValue(name, out var c) ? c + 1 : 1;
// Bare (unique) names are claimed first so a generated "{name}_{k}" can never shadow one.
var used = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var name in list)
if (counts[name] == 1)
used.Add(name);
var nextOrdinal = new Dictionary(StringComparer.OrdinalIgnoreCase);
var result = new string[list.Count];
for (var i = 0; i < list.Count; i++)
{
var name = list[i];
if (counts[name] == 1)
{
result[i] = name;
continue;
}
var k = nextOrdinal.TryGetValue(name, out var n) ? n : 1;
string segment;
do { segment = $"{name}_{k}"; k++; }
while (!used.Add(segment));
nextOrdinal[name] = k;
result[i] = segment;
}
return result;
}
}