using System.Security.Cryptography;
namespace XFEEliteDangerous.NativeFormat.Replication;
///
/// The pinned ServerAdmin secondary archive:12 fixed properties totalling448bits,
/// no optional mask,6 byte-string properties.6339D0 reads ordered fixed-bit runs
/// and length-delimited variable overrides;63B600 derives its9-bit index width.
/// This is per-peer control metadata, never a world or vessel state.
///
public sealed class EdServerAdminSecondaryArchive:IDisposable
{
public const int FixedBits=448,StringCount=6,IndexBits=9,MaximumStates=32;
private readonly ulong reference;
private readonly ushort version;
private readonly byte[] fixedDefaults;
private readonly bool defaultsVerified;
private readonly byte defaultKnownStrings;
private readonly Dictionary states=[];
private readonly Queue order=[];
private bool disposed;
public int StateCount=>states.Count;
public ushort LatestSequence{get;private set;}
public EdServerAdminSecondaryArchive(ulong reference,ushort version,ReadOnlySpan verifiedFixedDefaults=default,byte verifiedEmptyStringMask=63)
{
if(reference is 0 or >0xFFFFFFFFFFFF||verifiedFixedDefaults.Length is not(0 or FixedBits/8)||verifiedEmptyStringMask>63)throw new ArgumentOutOfRangeException(nameof(reference));
this.reference=reference;this.version=version;defaultsVerified=!verifiedFixedDefaults.IsEmpty;
fixedDefaults=defaultsVerified?verifiedFixedDefaults.ToArray():new byte[FixedBits/8];
defaultKnownStrings=verifiedEmptyStringMask;
}
public EdServerAdminSecondaryArchive Fork()
{
ObjectDisposedException.ThrowIf(disposed,this);var clone=new EdServerAdminSecondaryArchive(reference,version,defaultsVerified?fixedDefaults:[],defaultKnownStrings){LatestSequence=LatestSequence};
foreach(var sequence in order){clone.states.Add(sequence,states[sequence].Copy());clone.order.Enqueue(sequence);}return clone;
}
public void Apply(EdServerObjectUpdate update)
{
ObjectDisposedException.ThrowIf(disposed,this);ArgumentNullException.ThrowIfNull(update);
if(update.ObjectReference!=reference||!update.SecondaryArchive||update.ConstructionVersion!=version)
throw new InvalidDataException("controller-secondary-schema-mismatch");
var bytes=update.CopyPayload();byte[]? digest=null;State? candidate=null;
try
{
digest=SHA256.HashData(bytes);
states.TryGetValue(update.Sequence,out var prior);
if(prior is not null&&CryptographicOperations.FixedTimeEquals(prior.Digest,digest))return;
var p=0;var baseSequence=(ushort)Read(bytes,ref p,10,update.PayloadBits);
if(baseSequence==0)candidate=new(){Fixed=fixedDefaults.ToArray(),KnownStrings=defaultKnownStrings};
else if(states.TryGetValue(baseSequence,out var baseline))candidate=baseline.Copy();
else throw new NotSupportedException("controller-secondary-base-unavailable");
var previousEnd=-1;var seenVariables=new HashSet();var suppliedFixedBits=0;
while(pFixedBits)throw new InvalidDataException("controller-secondary-fixed-run");
for(var bit=index;bit=StringCount||!seenVariables.Add(variable))throw new InvalidDataException("controller-secondary-variable-index");
var payloadLength=(long)Read(bytes,ref p,32,update.PayloadBits);
if(payloadLength<32||payloadLength>update.PayloadBits-p)throw new InvalidDataException("controller-secondary-variable-length");
var end=p+(int)payloadLength;
var length=(long)Read(bytes,ref p,32,end);
if(length>1024||length*8!=end-p)throw new InvalidDataException("controller-secondary-string-length");
var value=new byte[(int)length];
for(var i=0;iprior.Strings[index].AsSpan().SequenceEqual(candidate.Strings[index])))
throw new InvalidDataException("controller-secondary-changed-duplicate");
return;
}
CryptographicOperations.ZeroMemory(candidate.Digest);candidate.Digest=digest;digest=null;
states.Add(update.Sequence,candidate);order.Enqueue(update.Sequence);candidate=null;LatestSequence=update.Sequence;
while(states.Count>MaximumStates){var sequence=order.Dequeue();states.Remove(sequence,out var expired);expired!.Dispose();}
}
finally{CryptographicOperations.ZeroMemory(bytes);if(digest is not null)CryptographicOperations.ZeroMemory(digest);candidate?.Dispose();}
}
public byte[] CopyFixedState(ushort sequence)
{ObjectDisposedException.ThrowIf(disposed,this);return states[sequence].Fixed.ToArray();}
public byte[] CopyString(ushort sequence,int index)
{ObjectDisposedException.ThrowIf(disposed,this);if(index is <0 or >=StringCount)throw new ArgumentOutOfRangeException(nameof(index));return states[sequence].Strings[index].ToArray();}
public void Dispose()
{if(disposed)return;disposed=true;foreach(var state in states.Values)state.Dispose();states.Clear();order.Clear();CryptographicOperations.ZeroMemory(fixedDefaults);LatestSequence=0;}
private static ulong Read(ReadOnlySpan bytes,ref int p,int count,int end)
{
if(count is <0 or >64||p<0||(long)p+count>end||(long)end>bytes.Length*8)throw new InvalidDataException("controller-secondary-truncated");
ulong value=0;for(var i=0;i>(p%8)&1)<Array.Empty()).ToArray();
internal State Copy()=>new(){Fixed=Fixed.ToArray(),Digest=Digest.ToArray(),KnownStrings=KnownStrings,Strings=Strings.Select(value=>value.ToArray()).ToArray()};
public void Dispose(){CryptographicOperations.ZeroMemory(Fixed);CryptographicOperations.ZeroMemory(Digest);foreach(var value in Strings)CryptographicOperations.ZeroMemory(value);}
}
}