namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
///
/// One PMC byte-level read request a caller wants to satisfy. is
/// how many consecutive PMC bytes the caller's tag occupies (e.g. Bit/Byte = 1, Int16 = 2,
/// Int32/Float32 = 4, Float64 = 8). is the caller's row index
/// in the batch — the coalescer carries it through to the planned group so the driver can
/// fan-out the slice back to the original snapshot slot.
///
///
/// Bit-addressed PMC tags (e.g. R100.3) supply their parent byte (100) +
/// ByteWidth = 1; the slice-then-mask happens in the existing decode path, so
/// the coalescer doesn't need to know about the bit index.
///
public sealed record PmcAddressRequest(
string Letter,
int PathId,
int ByteNumber,
int ByteWidth,
int OriginalIndex);
///
/// One member of a coalesced PMC range — the original index + the byte offset within
/// the planned range buffer where the member's bytes start. The caller slices
/// buffer[Offset .. Offset + ByteWidth] to recover the per-tag wire-shape bytes.
///
public sealed record PmcRangeMember(int OriginalIndex, int Offset, int ByteWidth);
///
/// One coalesced PMC range — a single FOCAS pmc_rdpmcrng wire call satisfies
/// every member. is bounded by .
///
public sealed record PmcRangeGroup(
string Letter,
int PathId,
int StartByte,
int ByteCount,
IReadOnlyList Members);
///
/// Plans one or more coalesced PMC range reads from a flat batch of per-tag requests.
/// Same-letter / same-path requests whose byte ranges overlap or whose gap is no larger
/// than are merged into a single wire call up to a
/// cap (issue #266).
///
///
/// The cap matches the conservative ceiling Fanuc spec lists for
/// pmc_rdpmcrng — most controllers accept larger ranges but 256 is the lowest
/// common denominator across 0i / 16i / 30i firmware. Splitting on the cap is fine —
/// each partition still saves N-1 round trips relative to per-byte reads.
///
/// The = 16 mirrors the Modbus coalescer's bridge
/// policy: small gaps are cheaper to over-read (one extra wire call vs. several short
/// ones) but unbounded bridging would pull large unused regions over the wire on sparse
/// PMC layouts.
///
public static class FocasPmcCoalescer
{
/// Maximum bytes per coalesced range — conservative ceiling for older Fanuc firmware.
public const int MaxRangeBytes = 256;
/// Maximum gap (in bytes) bridged between consecutive sub-requests within a group.
public const int MaxBridgeGap = 16;
///
/// Plan range reads from . Group key is
/// (Letter, PathId). Within a group, requests are sorted by start byte then
/// greedily packed into ranges that respect +
/// .
///
public static IReadOnlyList Plan(IEnumerable addresses)
{
ArgumentNullException.ThrowIfNull(addresses);
var groups = new List();
var byKey = addresses
.Where(a => !string.IsNullOrEmpty(a.Letter) && a.ByteWidth > 0 && a.ByteNumber >= 0)
.GroupBy(a => (Letter: a.Letter.ToUpperInvariant(), a.PathId));
foreach (var key in byKey)
{
var sorted = key.OrderBy(a => a.ByteNumber).ThenBy(a => a.OriginalIndex).ToList();
var pending = new List();
var rangeStart = -1;
var rangeEnd = -1;
void Flush()
{
if (pending.Count == 0) return;
var members = pending.Select(p =>
new PmcRangeMember(p.OriginalIndex, p.ByteNumber - rangeStart, p.ByteWidth)).ToList();
groups.Add(new PmcRangeGroup(key.Key.Letter, key.Key.PathId, rangeStart,
rangeEnd - rangeStart + 1, members));
pending.Clear();
rangeStart = -1;
rangeEnd = -1;
}
foreach (var req in sorted)
{
var reqStart = req.ByteNumber;
var reqEnd = req.ByteNumber + req.ByteWidth - 1;
if (pending.Count == 0)
{
rangeStart = reqStart;
rangeEnd = reqEnd;
pending.Add(req);
continue;
}
// Bridge if the gap between the existing range end + this request's start is
// within the bridge cap. Overlapping or contiguous ranges always bridge
// (gap <= 0). The cap is enforced on the projected union: extending the range
// must not exceed MaxRangeBytes from rangeStart.
var gap = reqStart - rangeEnd - 1;
var projectedEnd = Math.Max(rangeEnd, reqEnd);
var projectedSize = projectedEnd - rangeStart + 1;
if (gap <= MaxBridgeGap && projectedSize <= MaxRangeBytes)
{
rangeEnd = projectedEnd;
pending.Add(req);
}
else
{
Flush();
rangeStart = reqStart;
rangeEnd = reqEnd;
pending.Add(req);
}
}
Flush();
}
return groups;
}
///
/// The number of consecutive PMC bytes a tag of occupies on
/// the wire. Used by the driver to populate
/// before planning. Bit-addressed tags supply 1 here — the bit-extract happens
/// in the decode path after the slice.
///
public static int ByteWidth(FocasDataType type) => type switch
{
FocasDataType.Bit or FocasDataType.Byte => 1,
FocasDataType.Int16 => 2,
FocasDataType.Int32 or FocasDataType.Float32 => 4,
FocasDataType.Float64 => 8,
_ => 1,
};
}