using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
///
/// The bucket layout of a processed (aggregate) read. Bucket i = [start + i·interval,
/// start + (i+1)·interval) (start-inclusive, end-exclusive to the tick; the last bucket is truncated at
/// end). An interval <= 0 ⇒ one interval [start, end). start == end ⇒ no intervals.
/// end < start (reverse) ⇒ the window is swapped, computed forward, and assembled in descending bucket
/// order (buckets stay anchored at the earlier time).
///
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;
}
/// The earlier end of the window (the forward start).
public DateTime StartUtc { get; }
/// The later end of the window (the forward, exclusive end).
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; }
/// as 100 ns ticks since the Unix epoch.
public long StartUnixTicks => (StartUtc - DateTime.UnixEpoch).Ticks;
/// 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 start time of bucket (UTC kind).
/// Zero-based bucket index.
/// The bucket start.
public DateTime BucketStart(long index) =>
DateTime.SpecifyKind(new DateTime(StartUtc.Ticks + index * IntervalTicks), DateTimeKind.Utc);
/// One snapshot per bucket in window order; missing buckets get ; reversed if the window was.
/// Evaluated snapshots of the non-empty buckets, by bucket index.
/// Builds the snapshot of a bucket with no row, from its start time.
/// 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++)
{
var bucketStart = BucketStart(i);
result.Add(byBucket.TryGetValue(i, out var v) ? v : empty(bucketStart));
}
if (Reversed) result.Reverse();
return result;
}
}