using System;
using System.IO;
using static MLAPI.Serialization.Arithmetic;
namespace MLAPI.Serialization
{
///
/// A buffer that can be used at the bit level
///
public class NetworkBuffer : Stream
{
private const int k_InitialCapacity = 16;
private const float k_InitialGrowthFactor = 2.0f;
private byte[] m_Target;
///
/// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data.
///
/// Initial capacity of buffer in bytes.
/// Factor by which buffer should grow when necessary.
public NetworkBuffer(int capacity, float growthFactor)
{
m_Target = new byte[capacity];
GrowthFactor = growthFactor;
Resizable = true;
}
///
/// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data.
///
/// Factor by which buffer should grow when necessary.
public NetworkBuffer(float growthFactor) : this(k_InitialCapacity, growthFactor) { }
///
/// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data.
///
///
public NetworkBuffer(int capacity) : this(capacity, k_InitialGrowthFactor) { }
///
/// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data.
///
public NetworkBuffer() : this(k_InitialCapacity, k_InitialGrowthFactor) { }
///
/// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data.
/// NOTE: when using a pre-allocated buffer, the buffer will not grow!
///
/// Pre-allocated buffer to write to
public NetworkBuffer(byte[] target)
{
m_Target = target;
Resizable = false;
BitLength = (ulong)(target.Length << 3);
}
internal void SetTarget(byte[] target)
{
m_Target = target;
BitLength = (ulong)(target.Length << 3);
Position = 0;
}
///
/// Whether or not the buffer will grow the buffer to accomodate more data.
///
public bool Resizable { get; }
private float m_GrowthFactor;
///
/// Factor by which buffer should grow when necessary.
///
public float GrowthFactor { set { m_GrowthFactor = value <= 1 ? 1.5f : value; } get { return m_GrowthFactor; } }
///
/// Whether or not buffeer supports reading. (Always true)
///
public override bool CanRead => true;
///
/// Whether or not or there is any data to be read from the buffer.
///
public bool HasDataToRead => Position < Length;
///
/// Whether or not seeking is supported by this buffer. (Always true)
///
public override bool CanSeek => true;
///
/// Whether or not this buffer can accept new data. NOTE: this will return true even if only fewer than 8 bits can be written!
///
public override bool CanWrite => !BitAligned || Position < m_Target.LongLength || Resizable;
///
/// Current buffer size. The buffer will not be resized (if possible) until Position is equal to Capacity and an attempt to write data is made.
///
public long Capacity
{
get => m_Target.LongLength; // Optimized CeilingExact
set
{
if (value < Length) throw new ArgumentOutOfRangeException("New capcity too small!");
SetCapacity(value);
}
}
///
/// The current length of data considered to be "written" to the buffer.
///
public override long Length { get => Div8Ceil(BitLength); }
///
/// The index that will be written to when any call to write data is made to this buffer.
///
public override long Position { get => (long)(BitPosition >> 3); set => BitPosition = (ulong)value << 3; }
///
/// Bit offset into the buffer that new data will be written to.
///
public ulong BitPosition { get; set; }
///
/// Length of data (in bits) that is considered to be written to the buffer.
///
public ulong BitLength { get; private set; }
///
/// Whether or not the current BitPosition is evenly divisible by 8. I.e. whether or not the BitPosition is at a byte boundary.
///
public bool BitAligned { get => (BitPosition & 7) == 0; }
///
/// Flush buffer. This does nothing since data is written directly to a byte buffer.
///
public override void Flush() { } // NOP
///
/// Grow buffer if possible. According to Max(bufferLength, 1) * growthFactor^Ceil(newContent/Max(bufferLength, 1))
///
/// How many new values need to be accomodated (at least).
//private void Grow(long newContent) => SetCapacity(Math.Max(target.LongLength, 1) * (long)Math.Pow(GrowthFactor, CeilingExact(newContent, Math.Max(target.LongLength, 1))));
/*
private void Grow(long newContent)
{
float grow = newContent / 64;
if (((long)grow) != grow) grow += 1;
SetCapacity((Capacity + 64) * (long)grow);
}
*/
private void Grow(long newContent)
{
long value = newContent + Capacity;
long newCapacity = value;
if (newCapacity < 256) newCapacity = 256;
// We are ok with this overflowing since the next statement will deal
// with the cases where _capacity*2 overflows.
if (newCapacity < Capacity * 2) newCapacity = Capacity * 2;
// We want to expand the array up to Array.MaxArrayLengthOneDimensional
// And we want to give the user the value that they asked for
if ((uint)(Capacity * 2) > int.MaxValue) newCapacity = value > int.MaxValue ? value : int.MaxValue;
SetCapacity(newCapacity);
}
///
/// Read a misaligned byte. WARNING: If the current BitPosition isn't byte misaligned,
/// avoid using this method as it may cause an IndexOutOfBoundsException in such a case.
///
/// A byte extracted from up to two separate buffer indices.
private byte ReadByteMisaligned()
{
int mod = (int)(BitPosition & 7);
return (byte)((m_Target[(int)Position] >> mod) | (m_Target[(int)(BitPosition += 8) >> 3] << (8 - mod)));
}
///
/// Read an aligned byte from the buffer. It's recommended to not use this when the BitPosition is byte-misaligned.
///
/// The byte stored at the current Position index
private byte ReadByteAligned() => m_Target[Position++];
///
/// Read a byte as a byte. This is just for internal use so as to minimize casts (cuz they ugly af).
///
///
internal byte ReadByteInternal() => BitAligned ? ReadByteAligned() : ReadByteMisaligned();
///
/// Read a byte from the buffer. This takes into account possible byte misalignment.
///
/// A byte from the buffer or, if a byte can't be read, -1.
public override int ReadByte() => HasDataToRead ? BitAligned ? ReadByteAligned() : ReadByteMisaligned() : -1;
///
/// Peeks a byte without advancing the position
///
/// The peeked byte
public int PeekByte() =>
HasDataToRead
? BitAligned ? m_Target[Position] :
(byte)((m_Target[(int)Position] >> (int)(BitPosition & 7)) | (m_Target[(int)(BitPosition + 8) >> 3] << (8 - (int)(BitPosition & 7))))
: -1;
///
/// Read a single bit from the buffer.
///
/// A bit in bool format. (True represents 1, False represents 0)
public bool ReadBit() => (m_Target[Position] & (1 << (int)(BitPosition++ & 7))) != 0;
///
/// Read a subset of the buffer buffer and write the contents to the supplied buffer.
///
/// Buffer to copy data to.
/// Offset into the buffer to write data to.
/// How many bytes to attempt to read.
/// Amount of bytes read.
public override int Read(byte[] buffer, int offset, int count)
{
int tLen = Math.Min(count, (int)(m_Target.LongLength - Position) - ((BitPosition & 7) == 0 ? 0 : 1));
for (int i = 0; i < tLen; ++i) buffer[offset + i] = ReadByteInternal();
return tLen;
}
///
/// Set position in buffer to read from/write to.
///
/// Offset from position origin.
/// How to calculate offset.
/// The new position in the buffer that data will be written to.
public override long Seek(long offset, SeekOrigin origin)
{
return (long)((
BitPosition =
(
origin == SeekOrigin.Current ? offset > 0 ? Math.Min(BitPosition + ((ulong)offset << 3), (ulong)m_Target.Length << 3) :
(offset ^ SIGN_BIT_64) > Position ? 0UL :
BitPosition - (ulong)((offset ^ SIGN_BIT_64) << 3) :
origin == SeekOrigin.Begin ? (ulong)Math.Max(0, offset) << 3 :
(ulong)Math.Max(m_Target.Length - offset, 0) << 3
)) >> 3) + (long)((BitPosition & 1UL) | ((BitPosition >> 1) & 1UL) | ((BitPosition >> 2) & 1UL));
}
///
/// Set the capacity of the internal buffer.
///
/// New capacity of the buffer
private void SetCapacity(long value)
{
if (!Resizable) throw new NotSupportedException("Can't resize non resizable buffer"); // Don't do shit because fuck you (comment by @GabrielTofvesson -TwoTen)
byte[] newTarg = new byte[value];
long len = Math.Min(value, m_Target.LongLength);
Buffer.BlockCopy(m_Target, 0, newTarg, 0, (int)len);
if (value < m_Target.LongLength) BitPosition = (ulong)value << 3;
m_Target = newTarg;
}
///
/// Set length of data considered to be "written" to the buffer.
///
/// New length of the written data.
public override void SetLength(long value)
{
if (value < 0) throw new IndexOutOfRangeException("Cannot set a negative length!");
if (value > Capacity) Grow(value - Capacity);
BitLength = (ulong)value << 3;
BitPosition = Math.Min((ulong)value << 3, BitPosition);
}
///
/// Write data from the given buffer to the internal buffer.
///
/// Buffer to write from.
/// Offset in given buffer to start reading from.
/// Amount of bytes to read copy from given buffer to buffer.
public override void Write(byte[] buffer, int offset, int count)
{
// Check bit alignment. If misaligned, each byte written has to be misaligned
if (BitAligned)
{
if (Position + count >= m_Target.Length) Grow(count);
Buffer.BlockCopy(buffer, offset, m_Target, (int)Position, count);
Position += count;
}
else
{
if (Position + count + 1 >= m_Target.Length) Grow(count);
for (int i = 0; i < count; ++i) WriteMisaligned(buffer[offset + i]);
}
if (BitPosition > BitLength) BitLength = BitPosition;
}
///
/// Write byte value to the internal buffer.
///
/// The byte value to write.
public override void WriteByte(byte value)
{
// Check bit alignment. If misaligned, each byte written has to be misaligned
if (BitAligned)
{
if (Position + 1 >= m_Target.Length) Grow(1);
m_Target[Position] = value;
Position += 1;
}
else
{
if (Position + 1 + 1 >= m_Target.Length) Grow(1);
WriteMisaligned(value);
}
if (BitPosition > BitLength) BitLength = BitPosition;
}
///
/// Write a misaligned byte. NOTE: Using this when the bit position isn't byte-misaligned may cause an IndexOutOfBoundsException! This does not update the current Length of the buffer.
///
/// Value to write
private void WriteMisaligned(byte value)
{
int off = (int)(BitPosition & 7);
int shift1 = 8 - off;
m_Target[Position + 1] = (byte)((m_Target[Position + 1] & (0xFF << off)) | (value >> shift1));
m_Target[Position] = (byte)((m_Target[Position] & (0xFF >> shift1)) | (value << off));
BitPosition += 8;
}
///
/// Write a byte (in an int format) to the buffer. This does not update the current Length of the buffer.
///
/// Value to write
private void WriteIntByte(int value) => WriteBytePrivate((byte)value);
///
/// Write a byte (in a ulong format) to the buffer. This does not update the current Length of the buffer.
///
/// Value to write
private void WriteULongByte(ulong byteValue) => WriteBytePrivate((byte)byteValue);
///
/// Write a byte to the buffer. This does not update the current Length of the buffer.
///
/// Value to write
private void WriteBytePrivate(byte value)
{
if (Div8Ceil(BitPosition) == m_Target.LongLength) Grow(1);
if (BitAligned)
{
m_Target[Position] = value;
BitPosition += 8;
}
else WriteMisaligned(value);
UpdateLength();
}
///
/// Write data from the given buffer to the internal buffer.
///
/// Buffer to write from.
public void Write(byte[] buffer) => Write(buffer, 0, buffer.Length);
///
/// Write a single bit to the buffer
///
/// Value of the bit. True represents 1, False represents 0
public void WriteBit(bool bit)
{
if (BitAligned && Position == m_Target.Length) Grow(1);
int offset = (int)(BitPosition & 7);
long pos = Position;
++BitPosition;
m_Target[pos] = (byte)(bit ? (m_Target[pos] & ~(1 << offset)) | (1 << offset) : (m_Target[pos] & ~(1 << offset)));
UpdateLength();
}
///
/// Copy data from another stream
///
/// Stream to copy from
/// How many bytes to read. Set to value less than one to read until ReadByte returns -1
public void CopyFrom(Stream s, int count = -1)
{
if (s is NetworkBuffer b) Write(b.m_Target, 0, count < 0 ? (int)b.Length : count);
else
{
long currentPosition = s.Position;
s.Position = 0;
int read;
bool readToEnd = count < 0;
while ((readToEnd || count-- > 0) && (read = s.ReadByte()) != -1)
WriteIntByte(read);
UpdateLength();
s.Position = currentPosition;
}
}
///
/// Copies internal buffer to stream
///
/// The stream to copy to
/// The maximum amount of bytes to copy. Set to value less than one to copy the full length
#if !NET35
public new void CopyTo(Stream stream, int count = -1)
#else
public void CopyTo(Stream stream, int count = -1)
#endif
{
stream.Write(m_Target, 0, count < 0 ? (int)Length : count);
}
///
/// Copies urnead bytes from the source stream
///
/// The source stream to copy from
/// The max amount of bytes to copy
public void CopyUnreadFrom(Stream s, int count = -1)
{
long currentPosition = s.Position;
int read;
bool readToEnd = count < 0;
while ((readToEnd || count-- > 0) && (read = s.ReadByte()) != -1) WriteIntByte(read);
UpdateLength();
s.Position = currentPosition;
}
// TODO: Implement CopyFrom() for NetworkBuffer with bitCount parameter
///
/// Copys the bits from the provided NetworkBuffer
///
/// The buffer to copy from
/// The amount of data evel
/// Whether or not to copy at the bit level rather than the byte level
public void CopyFrom(NetworkBuffer buffer, int dataCount, bool copyBits)
{
if (!copyBits)
{
CopyFrom(buffer, dataCount);
}
else
{
ulong count = dataCount < 0 ? buffer.BitLength : (ulong)dataCount;
if (buffer.BitLength < count) throw new IndexOutOfRangeException("Attempted to read more data than is available");
Write(buffer.GetBuffer(), 0, (int)(count >> 3));
for (int i = (int)(count & 7); i >= 0; --i) WriteBit(buffer.ReadBit());
}
}
///
/// Update length of data considered to be "written" to the buffer.
///
private void UpdateLength()
{
if (BitPosition > BitLength) BitLength = BitPosition;
}
///
/// Get the internal buffer being written to by this buffer.
///
///
public byte[] GetBuffer() => m_Target;
///
/// Creates a copy of the internal buffer. This only contains the used bytes
///
/// A copy of used bytes in the internal buffer
public byte[] ToArray()
{
byte[] copy = new byte[Length];
Buffer.BlockCopy(m_Target, 0, copy, 0, (int)Length);
return copy;
}
///
/// Writes zeros to fill the last byte
///
public void PadBuffer()
{
while (!BitAligned)
{
WriteBit(false);
}
}
///
/// Reads zeros until the the buffer is byte aligned
///
public void SkipPadBits()
{
while (!BitAligned)
{
ReadBit();
}
}
///
/// Returns hex encoded version of the buffer
///
/// Hex encoded version of the buffer
public override string ToString() => BitConverter.ToString(m_Target, 0, (int)Length);
}
}