using System.Buffers.Binary;
namespace TravelEar.Core;
/// One Offset observation the Helper sends back to the mod.
/// The capture timestamp that travelled with the Sink frame (the mod's Stopwatch ticks at OpusEncoder.Encode).
/// When the frame's first sample leaves the Helper's endpoint, in the same clock (both processes read the machine's performance counter).
public readonly record struct OffsetReport(long CaptureTimestamp, long RenderTimestamp);
///
/// Framing for the return pipe (TravelEar.Sink.Back): fixed 20-byte little-endian records,
/// magic then the two timestamps. Shared by the Helper (writer) and the mod (reader).
///
public static class OffsetReportFrame
{
/// "TEOF" (TravelEar Offset) read as a little-endian uint32.
public const uint Magic = 0x464F4554;
public const int Size = 4 + 8 + 8;
// [impl->REQ-OFFSET-MEASURE]
public static void Write(Span destination, in OffsetReport report)
{
if (destination.Length < Size) throw new ArgumentException("Destination too small.", nameof(destination));
BinaryPrimitives.WriteUInt32LittleEndian(destination, Magic);
BinaryPrimitives.WriteInt64LittleEndian(destination.Slice(4), report.CaptureTimestamp);
BinaryPrimitives.WriteInt64LittleEndian(destination.Slice(12), report.RenderTimestamp);
}
/// Parses one record. False if the bytes are too short or carry no magic.
public static bool TryRead(ReadOnlySpan source, out OffsetReport report)
{
report = default;
if (source.Length < Size) return false;
if (BinaryPrimitives.ReadUInt32LittleEndian(source) != Magic) return false;
report = new OffsetReport(
BinaryPrimitives.ReadInt64LittleEndian(source.Slice(4)),
BinaryPrimitives.ReadInt64LittleEndian(source.Slice(12)));
return true;
}
/// Writes one record to a stream. must hold bytes.
public static void WriteTo(Stream stream, in OffsetReport report, byte[] scratch)
{
Write(scratch, report);
stream.Write(scratch, 0, Size);
}
///
/// Reads exactly one record. False on a clean end-of-stream before a record starts; throws
/// on a bad magic or a record cut by end-of-stream.
///
public static bool TryReadFrom(Stream stream, out OffsetReport report, byte[] scratch)
{
report = default;
var total = 0;
while (total < Size)
{
var n = stream.Read(scratch, total, Size - total);
if (n <= 0) break;
total += n;
}
if (total == 0) return false;
if (total < Size) throw new InvalidDataException("Offset stream ended inside a record.");
if (!TryRead(scratch, out report)) throw new InvalidDataException("Invalid Offset record.");
return true;
}
}