using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
///
/// The bucket layout of a processed (aggregate) read, per OPC UA Part 13 / the SDK's AggregateCalculator.
///
///
///
/// Forward (start < end): the window is [start, end); bucket i =
/// [start + i·interval, start + (i+1)·interval), the last bucket truncated at end (the partial one is
/// the latest); each bucket's timestamp is its start (earlier edge); results ascend.
///
///
/// Reverse (end < start, #677): the read is anchored at the request's start — its later
/// time — and steps back. The window is (end, start] (Part 11: the request's start is inclusive, its end
/// exclusive, so a reverse read includes its later time and excludes its earlier one — the same window as a reverse
/// raw read); result k = (start − (k+1)·interval, start − k·interval], clipped at end (the
/// partial bucket is the earliest); each bucket's timestamp is its later edge start − k·interval;
/// results descend. This is exactly the SDK's CreateSlice / GetTimestamp / backward
/// GetValues.
///
///
/// Internally buckets are indexed in ascending time (0 = earliest) for both directions;
/// reverses the list for a reverse read. / are always the earlier / later
/// window edges. An interval <= 0 ⇒ one interval over the whole window. start == end ⇒ no intervals.
///
///
public sealed class BucketPlan
{
/// Spec §9.7: a processed read covering more intervals than this is refused.
public const int MaxBuckets = 100_000;
private BucketPlan(DateTime startUtc, DateTime endUtc, bool reversed, long intervalTicks, long bucketCount)
{
StartUtc = startUtc;
EndUtc = endUtc;
Reversed = reversed;
IntervalTicks = intervalTicks;
BucketCount = bucketCount;
// Forward buckets are anchored at the earlier edge; reverse ones at the later edge, so the grid origin sits a
// whole number of intervals before it (possibly before the window start — the earliest bucket is clipped).
OriginTicks = reversed ? endUtc.Ticks - bucketCount * intervalTicks : startUtc.Ticks;
}
/// The earlier edge of the window.
public DateTime StartUtc { get; }
/// The later edge of the window.
public DateTime EndUtc { get; }
/// True when the request came in reversed (end < start).
public bool Reversed { get; }
/// Bucket width in 100 ns ticks (clamped to the window; 0 for an empty window).
public long IntervalTicks { get; }
/// Number of buckets (0 for an empty window).
public long BucketCount { get; }
///
/// The tick at which the (unclipped) bucket grid starts: forward, EndUtc − n·interval
/// reverse. May precede (and even ), so it is a raw tick count.
///
public long OriginTicks { get; }
/// as 100 ns ticks since the Unix epoch.
public long StartUnixTicks => (StartUtc - DateTime.UnixEpoch).Ticks;
///
/// The constant a backend adds to elapsed = t − StartUtc (ticks) before the integer division by
/// to get a sample's ascending bucket index: 0 forward ([lo, hi) buckets);
/// (StartUtc − OriginTicks) − 1 reverse, where buckets are (lo, hi] — index ⌈(t − origin)/I⌉ − 1
/// = (t − origin − 1) / I. Never makes the dividend negative for an in-window sample (reverse excludes
/// t == StartUtc).
///
public long BucketIndexAdjustTicks => Reversed ? StartUtc.Ticks - OriginTicks - 1 : 0;
///
/// minus : the base a backend subtracts from a
/// sample's Unix-epoch ticks to get the same dividend ((t − base) / IntervalTicks).
///
public long BucketBaseUnixTicks => StartUnixTicks - BucketIndexAdjustTicks;
/// Plans the buckets of a processed read.
/// Requested start.
/// Requested end (earlier than start for a reverse read).
/// Bucket width; <= 0 means one bucket over the whole window.
/// The plan.
/// The window holds more than intervals
/// (parameter name interval).
public static BucketPlan Create(DateTime startUtc, DateTime endUtc, TimeSpan interval)
{
var reverse = endUtc < startUtc;
var (start, end) = reverse ? (endUtc, startUtc) : (startUtc, endUtc);
var windowTicks = end.Ticks - start.Ticks;
// Everything stays in 100 ns ticks (≤ ≈3.2e18 across 0001–9999, inside a long) — no ns scaling that could
// wrap. An interval wider than the window is one bucket, so clamp it to the window; the ceiling division
// is written without "+ interval - 1" so a TimeSpan near MaxValue cannot overflow either.
var intervalTicks = interval > TimeSpan.Zero ? Math.Min(interval.Ticks, windowTicks) : windowTicks;
long bucketCount = windowTicks == 0 ? 0 : windowTicks / intervalTicks + (windowTicks % intervalTicks == 0 ? 0 : 1);
if (bucketCount > MaxBuckets)
throw new ArgumentOutOfRangeException(nameof(interval), bucketCount,
$"A processed read may cover at most {MaxBuckets} intervals; this window holds {bucketCount}.");
return new BucketPlan(start, end, reverse, intervalTicks, bucketCount);
}
/// The earlier edge of bucket (ascending index), in ticks, clipped to the window.
/// Zero-based ascending bucket index.
/// The earlier edge.
public long LowerTicks(long index) => Math.Max(OriginTicks + index * IntervalTicks, StartUtc.Ticks);
/// The later edge of bucket (ascending index), in ticks, clipped to the window.
/// Zero-based ascending bucket index.
/// The later edge.
public long UpperTicks(long index) => Math.Min(OriginTicks + (index + 1) * IntervalTicks, EndUtc.Ticks);
///
/// The result timestamp of bucket (UTC kind): its earlier edge forward, its later edge
/// reverse (the SDK's GetTimestamp).
///
/// Zero-based ascending bucket index.
/// The bucket's timestamp.
public DateTime BucketTimestamp(long index) =>
new(Reversed ? UpperTicks(index) : LowerTicks(index), DateTimeKind.Utc);
/// True when bucket is shorter than the interval (the clipped one).
/// Zero-based ascending bucket index.
/// Whether the bucket is truncated.
public bool IsTruncated(long index) => UpperTicks(index) - LowerTicks(index) < IntervalTicks;
///
/// The ascending bucket index holding a sample at , or null when it is outside the window
/// ([start, end) forward, (start, end] reverse). The C# statement of the bucket math every backend's
/// SQL mirrors.
///
/// Sample time.
/// The bucket index, or null.
public long? IndexOf(DateTime t)
{
var x = t.Ticks;
if (BucketCount == 0) return null;
var inside = Reversed ? x > StartUtc.Ticks && x <= EndUtc.Ticks : x >= StartUtc.Ticks && x < EndUtc.Ticks;
if (!inside) return null;
return (x - StartUtc.Ticks + BucketIndexAdjustTicks) / IntervalTicks;
}
///
/// True when the earliest stored sample of the tag lies strictly inside bucket — the data
/// starts part-way through the interval (no bounding value before it), so Part 13 / the SDK set the Partial bit.
///
/// Zero-based ascending bucket index.
/// The tag's earliest stored sample (any quality), or null when it has none.
/// Whether the data starts inside the bucket.
public bool DataStartsInside(long index, DateTime? earliestSampleUtc) =>
earliestSampleUtc is { } e && e.Ticks > LowerTicks(index) && e.Ticks < UpperTicks(index);
///
/// True when the latest stored sample of the tag lies in bucket : the data ends part-way
/// through the interval, so Part 13 §5.3.3.2 / the SDK set the Partial bit (#680). Forward the earlier edge is
/// included (the SDK's m_endOfData >= startTime && m_endOfData < endTime); reverse the sample
/// must lie strictly inside (the SDK's reverse read keys m_startOfData — a strict test — on the first value it
/// is fed, the latest).
///
/// Zero-based ascending bucket index.
/// The tag's latest stored sample (any quality), or null when it has none.
/// Whether the data ends inside the bucket.
public bool DataEndsInside(long index, DateTime? latestSampleUtc) =>
latestSampleUtc is { } l
&& (Reversed ? l.Ticks > LowerTicks(index) : l.Ticks >= LowerTicks(index)) && l.Ticks < UpperTicks(index);
///
/// True when bucket overlaps the tag's data range [earliest, latest] — neither
/// wholly before the first sample nor wholly after the last (the SDK's GetValues null test:
/// start > last || end < first ⇒ no data, both edges inclusive in either direction). An empty bucket
/// inside the range is a Count of 0, not BadNoData (#680, Part 13 §5.4.3.21).
///
/// Zero-based ascending bucket index.
/// The tag's earliest stored sample (any quality), or null.
/// The tag's latest stored sample (any quality), or null.
/// Whether the bucket is within the data range.
public bool WithinDataRange(long index, DateTime? earliestSampleUtc, DateTime? latestSampleUtc) =>
earliestSampleUtc is { } e && latestSampleUtc is { } l && LowerTicks(index) <= l.Ticks && UpperTicks(index) >= e.Ticks;
/// One snapshot per bucket in result order; missing buckets get ; descending if reversed.
/// Evaluated snapshots of the non-empty buckets, by ascending bucket index.
/// Builds the snapshot of a bucket with no row, from its .
/// snapshots.
public IReadOnlyList Assemble(IReadOnlyDictionary byBucket,
Func empty)
{
ArgumentNullException.ThrowIfNull(byBucket);
ArgumentNullException.ThrowIfNull(empty);
var result = new List((int)BucketCount);
for (long i = 0; i < BucketCount; i++)
result.Add(byBucket.TryGetValue(i, out var v) ? v : empty(BucketTimestamp(i)));
if (Reversed) result.Reverse();
return result;
}
}