using System;
using NUnit.Framework;
using Unity.Collections.NotBurstCompatible;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine.TestTools;
using UnityEngine;

namespace Unity.Collections.Tests
{
    internal class DataStreamTests
    {
        internal class ReadAndWrite
        {
            [TestCase(ushort.MaxValue)]
            [TestCase(ushort.MinValue)]
            public void UShort(ushort expected)
            {
                bool Write(ref DataStreamWriter writer) => writer.WriteUShort(expected);
                CheckReadWrite(sizeof(ushort), reader => reader.ReadUShort(), Write, expected);
            }

            [TestCase((uint) 0b101010)]
            [TestCase((uint) 0b111111)]
            public void RawBits(uint expected)
            {
                bool Write(ref DataStreamWriter writer) => writer.WriteRawBits(expected, 6);
                CheckReadWrite(6, reader => reader.ReadRawBits(6), Write, expected);
            }

            [Test]
            public void RawBits_OutOfCapacity()
            {
                const uint expected = 0b101011001;
                var writer = new DataStreamWriter(1, Allocator.Temp);
                Assert.False(writer.WriteRawBits(expected, 9));
                Assert.True(writer.HasFailedWrites);
            }

            [TestCase(uint.MaxValue)]
            [TestCase(uint.MinValue)]
            public void UInt(uint expected)
            {
                bool Write(ref DataStreamWriter writer) => writer.WriteUInt(expected);
                CheckReadWrite(sizeof(uint), reader => reader.ReadUInt(), Write, expected);
            }

            [TestCase(float.MaxValue)]
            [TestCase(float.MinValue)]
            public void Float(float expected)
            {
                bool Write(ref DataStreamWriter writer) => writer.WriteFloat(expected);
                CheckReadWrite(sizeof(float), reader => reader.ReadFloat(), Write, expected);
            }

            [TestCase(short.MaxValue)]
            [TestCase(short.MinValue)]
            public void Short(short expected)
            {
                bool Write(ref DataStreamWriter writer) => writer.WriteShort(expected);
                CheckReadWrite(sizeof(short), reader => reader.ReadShort(), Write, expected);
            }

            [Test]
            public void FixedString32()
            {
                var expected = new FixedString32Bytes("This is a string");
                bool Write(ref DataStreamWriter writer) => writer.WriteFixedString32(expected);
                CheckReadWrite(expected.Length + FixedStringHeader, reader => reader.ReadFixedString32(), Write, expected);
            }

            [Test]
            public void FixedString64()
            {
                var expected = new FixedString64Bytes("This is a string");
                bool Write(ref DataStreamWriter writer) => writer.WriteFixedString64(expected);
                CheckReadWrite(expected.Length + FixedStringHeader, reader => reader.ReadFixedString64(), Write, expected);
            }

            [Test]
            public void FixedString128()
            {
                var expected = new FixedString128Bytes("This is a string");
                bool Write(ref DataStreamWriter writer) => writer.WriteFixedString128(expected);
                CheckReadWrite(expected.Length + FixedStringHeader, reader => reader.ReadFixedString128(), Write, expected);
            }

            [Test]
            public void FixedString512()
            {
                var expected = new FixedString512Bytes("This is a string");
                bool Write(ref DataStreamWriter writer) => writer.WriteFixedString512(expected);
                CheckReadWrite(expected.Length + FixedStringHeader, reader => reader.ReadFixedString512(), Write, expected);
            }

            [Test]
            public void FixedString4096()
            {
                var expected = new FixedString4096Bytes("This is a string");
                bool Write(ref DataStreamWriter writer) => writer.WriteFixedString4096(expected);
                CheckReadWrite(expected.Length + FixedStringHeader, reader => reader.ReadFixedString4096(), Write,
                    expected);
            }

            [Test]
            public void LongLooped()
            {
                const long baseVal = -99;
                const long expected = -1979;
                bool Write(ref DataStreamWriter writer, long value) => writer.WriteLong(value);
                long Read(ref DataStreamReader reader) => reader.ReadLong();
                CheckReadWriteLooped(sizeof(long), baseVal, expected, Write, Read, (l, u) => l + u);
            }
        }

        internal class ReadAndWriteNetworkOrder
        {
            [TestCase(int.MaxValue)]
            [TestCase(int.MinValue)]
            public void Int(int expected)
            {
                bool Write(ref DataStreamWriter writer) => writer.WriteIntNetworkByteOrder(expected);
                CheckReadWrite(sizeof(int), reader => reader.ReadIntNetworkByteOrder(), Write, expected);
            }

            [TestCase(uint.MaxValue)]
            [TestCase(uint.MinValue)]
            public void UInt(uint expected)
            {
                bool Write(ref DataStreamWriter writer) => writer.WriteUIntNetworkByteOrder(expected);
                CheckReadWrite(sizeof(uint), reader => reader.ReadUIntNetworkByteOrder(), Write, expected);
            }

            [TestCase(short.MaxValue)]
            [TestCase(short.MinValue)]
            public void Short(short expected)
            {
                bool Write(ref DataStreamWriter writer) => writer.WriteShortNetworkByteOrder(expected);
                CheckReadWrite(sizeof(short), reader => reader.ReadShortNetworkByteOrder(), Write, expected);
            }

            [TestCase(ushort.MaxValue)]
            [TestCase(ushort.MinValue)]
            public void UShort(ushort expected)
            {
                bool Write(ref DataStreamWriter writer) => writer.WriteUShortNetworkByteOrder(expected);
                CheckReadWrite(sizeof(ushort), reader => reader.ReadUShortNetworkByteOrder(), Write, expected);
            }

            [Test]
            public void ReadIncorrect()
            {
                var dataStream = new DataStreamWriter(4, Allocator.Temp);
                dataStream.WriteIntNetworkByteOrder(1979);
                dataStream.Flush();
                var reader = new DataStreamReader(dataStream.AsNativeArray());
                Assert.AreNotEqual(1979, reader.ReadInt());
            }
        }

        internal class ReadWritePacked
        {
            [Test]
            public void UInt()
            {
                const uint expected = 1979;
                const uint baseVal = 2000;
                bool Write(ref DataStreamWriter writer) => writer.WriteUInt(expected);
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model, uint value) => writer.WritePackedUInt(value, model);
                uint Read(ref DataStreamReader reader) => reader.ReadUInt();
                uint ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedUInt(model);
                CheckReadWritePackedLooped(sizeof(uint), baseVal, expected, Write, WritePacked, Read, ReadPacked, (l, u) => l + u);
            }

            [Test]
            public void IntExistingData()
            {
                unsafe
                {
                    var n = 300 * 4;
                    var data = stackalloc byte[n];
                    var compressionModel = StreamCompressionModel.Default;
                    var dataStream = new DataStreamWriter(data, n);
                    const int base_val = -10;
                    const int count = 20;
                    for (int i = 0; i < count; ++i)
                        dataStream.WritePackedInt(base_val + i, compressionModel);

                    dataStream.WriteInt((int)1979);
                    dataStream.Flush();

                    var na = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<byte>(data, n, Allocator.Invalid);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
                    NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref na, AtomicSafetyHandle.GetTempMemoryHandle());
#endif
                    var reader = new DataStreamReader(na);
                    for (int i = 0; i < count; ++i)
                    {
                        var val = reader.ReadPackedInt(compressionModel);
                        Assert.AreEqual(base_val + i, val);
                    }

                    Assert.AreEqual(1979, reader.ReadInt());
                }
            }

            [Test]
            public void Int()
            {
                const int expected = -1979;
                const int baseVal = -10;
                bool Write(ref DataStreamWriter writer) => writer.WriteInt(expected);
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model, int value) => writer.WritePackedInt(value, model);
                int Read(ref DataStreamReader reader) => reader.ReadInt();
                int ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedInt(model);
                CheckReadWritePackedLooped(sizeof(int), baseVal, expected, Write, WritePacked, Read, ReadPacked, (i, u) => (int)(i + u));
            }

            [Test]
            public void Long()
            {
                const long expected = -1979;
                const long baseVal = -99;
                bool Write(ref DataStreamWriter writer) => writer.WriteLong(expected);
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model, long value) => writer.WritePackedLong(value, model);
                long Read(ref DataStreamReader reader) => reader.ReadLong();
                long ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedLong(model);

                CheckReadWritePackedLooped(sizeof(long), baseVal, expected, Write, WritePacked, Read, ReadPacked, (l, u) => l + u);
            }

            [Test]
            public void ULong()
            {
                const ulong expected = 1979;
                const ulong baseVal = 2000;
                bool Write(ref DataStreamWriter writer) => writer.WriteULong(expected);
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model, ulong value) => writer.WritePackedULong(value, model);
                ulong Read(ref DataStreamReader reader) => reader.ReadULong();
                ulong ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedULong(model);
                CheckReadWritePackedLooped(sizeof(ulong), baseVal, expected, Write, WritePacked, Read, ReadPacked, (l, u) => l + u);
            }

            [Test]
            public void Float()
            {
                const float expected = 1979.1f;
                const float baseVal = 2000.1f;
                bool Write(ref DataStreamWriter writer) => writer.WriteFloat(expected);
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model, float value) => writer.WritePackedFloat(value, model);
                float Read(ref DataStreamReader reader) => reader.ReadFloat();
                float ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedFloat(model);
                CheckReadWritePackedLooped(sizeof(float), baseVal, expected, Write, WritePacked, Read, ReadPacked, (l, u) => l + u);
            }

            [Test]
            public void Double()
            {
                const double expected = 1979.1989;
                const double baseVal = 2000.2000;
                bool Write(ref DataStreamWriter writer) => writer.WriteDouble(expected);
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model, double value) => writer.WritePackedDouble(value, model);
                double Read(ref DataStreamReader reader) => reader.ReadDouble();
                double ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedDouble(model);
                CheckReadWritePackedLooped(sizeof(double), baseVal, expected, Write, WritePacked, Read, ReadPacked, (l, u) => l + u);
            }

            [Test]
            public void WriteOutSideOfCapacity_Fails()
            {
                var model = StreamCompressionModel.Default;
                var writer = new DataStreamWriter(sizeof(uint) / 2, Allocator.Temp);
                Assert.False(writer.HasFailedWrites);
                Assert.False(writer.WritePackedUInt(uint.MaxValue, model), "Writing a uint where there is no room should fail.");
                Assert.That(writer.HasFailedWrites);
            }

            [Test]
            [TestRequiresDotsDebugOrCollectionChecks]
            public void ReadOutSideOfCapacity_Fails()
            {
                var model = StreamCompressionModel.Default;
                var reader = new DataStreamReader(new NativeArray<byte>(0, Allocator.Temp));
                LogAssert.Expect(LogType.Error, "Trying to read 2 bits from a stream where only 0 are available");
                Assert.That(reader.ReadPackedUInt(model), Is.EqualTo(0));
                Assert.That(reader.HasFailedReads);
            }
        }

        internal class ReadWritePackedDelta
        {
            [Test]
            public void Int()
            {
                const int expected = int.MaxValue;
                const int baseline = int.MinValue;
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model) => writer.WritePackedIntDelta(expected, baseline, model);
                int ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedIntDelta(baseline, model);
                CheckReadWritePacked(sizeof(int), ReadPacked, WritePacked, expected);
            }

            [Test]
            public void Long()
            {
                const long expected = long.MaxValue;
                const long baseline = long.MinValue;
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model) => writer.WritePackedLongDelta(expected, baseline, model);
                long ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedLongDelta(baseline, model);
                CheckReadWritePacked(sizeof(long), ReadPacked, WritePacked, expected);
            }

            [Test]
            public void ULong()
            {
                const ulong expected = ulong.MaxValue;
                const ulong baseline = ulong.MinValue;
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model) => writer.WritePackedULongDelta(expected, baseline, model);
                ulong ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedULongDelta(baseline, model);
                CheckReadWritePacked(sizeof(ulong), ReadPacked, WritePacked, expected);
            }

            [Test]
            public void FixedString32()
            {
                var expected = new FixedString32Bytes("This is a string");
                var baseline = new FixedString32Bytes("This is another string");
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model) => writer.WritePackedFixedString32Delta(expected, baseline, model);
                FixedString32Bytes ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedFixedString32Delta(baseline, model);
                CheckReadWritePacked(expected.Length + FixedStringHeader, ReadPacked, WritePacked, expected);
            }

            [Test]
            public void FixedString32_LargerBaseline()
            {
                var expected = new FixedString32Bytes("This is another string");
                var baseline = new FixedString32Bytes("This is a string");
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model) => writer.WritePackedFixedString32Delta(expected, baseline, model);
                FixedString32Bytes ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedFixedString32Delta(baseline, model);
                CheckReadWritePacked(expected.Length + FixedStringHeader, ReadPacked, WritePacked, expected);
            }

            [Test]
            public void FixedString64()
            {
                var expected = new FixedString64Bytes("This is a string");
                var baseline = new FixedString64Bytes("This is another string");
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model) => writer.WritePackedFixedString64Delta(expected, baseline, model);
                FixedString64Bytes ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedFixedString64Delta(baseline, model);
                CheckReadWritePacked(expected.Length + FixedStringHeader, ReadPacked, WritePacked, expected);
            }

            [Test]
            public void FixedString128()
            {
                var expected = new FixedString128Bytes("This is a string");
                var baseline = new FixedString128Bytes("This is another string");
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model) => writer.WritePackedFixedString128Delta(expected, baseline, model);
                FixedString128Bytes ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedFixedString128Delta(baseline, model);
                CheckReadWritePacked(expected.Length + FixedStringHeader, ReadPacked, WritePacked, expected);
            }

            [Test]
            public void FixedString512()
            {
                var expected = new FixedString512Bytes("This is a string");
                var baseline = new FixedString512Bytes("This is another string");
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model) => writer.WritePackedFixedString512Delta(expected, baseline, model);
                FixedString512Bytes ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedFixedString512Delta(baseline, model);
                CheckReadWritePacked(expected.Length + FixedStringHeader, ReadPacked, WritePacked, expected);
            }

            [Test]
            public void FixedString4096()
            {
                var expected = new FixedString4096Bytes("This is a string");
                var baseline = new FixedString4096Bytes("This is another string");
                bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model) => writer.WritePackedFixedString4096Delta(expected, baseline, model);
                FixedString4096Bytes ReadPacked(ref DataStreamReader reader, StreamCompressionModel model) => reader.ReadPackedFixedString4096Delta(baseline, model);
                CheckReadWritePacked(expected.Length + FixedStringHeader, ReadPacked, WritePacked, expected);
            }

            [Test]
            public void Float_OutOfBoundsFails()
            {
                const float expected = float.MaxValue;
                const float baseline = float.MinValue;
                var model = StreamCompressionModel.Default;
                var writer = new DataStreamWriter(sizeof(float) / 2, Allocator.Temp);
                Assert.False(writer.HasFailedWrites);
                Assert.False(writer.WritePackedFloatDelta(expected, baseline, model));
                Assert.That(writer.HasFailedWrites);
            }

            [Test]
            public void Float_UnchangedData()
            {
                const float expected = float.MaxValue;
                const float baseline = float.MaxValue;
                var model = StreamCompressionModel.Default;
                var writer = new DataStreamWriter(1, Allocator.Temp);
                Assert.True(writer.WritePackedFloatDelta(expected, baseline, model));
                var reader = new DataStreamReader(writer.AsNativeArray());
                Assert.AreEqual(baseline, reader.ReadPackedFloatDelta(baseline, model));
                Assert.That(reader.GetBitsRead(), Is.EqualTo(1));
            }

            [Test]
            public void Float_ChangedData()
            {
                const float expected = float.MaxValue;
                const float baseline = float.MinValue;
                var model = StreamCompressionModel.Default;
                var writer = new DataStreamWriter(sizeof(float) + 1, Allocator.Temp);
                Assert.True(writer.WritePackedFloatDelta(expected, baseline, model));
                var reader = new DataStreamReader(writer.AsNativeArray());
                Assert.AreEqual(1, reader.ReadRawBits(1));
                var uf = new UIntFloat
                {
                    intValue = reader.ReadRawBits(32)
                };
                Assert.AreEqual(expected, uf.floatValue);
            }

            [Test]
            [TestRequiresDotsDebugOrCollectionChecks]
            public void UInt_OutOfCapacity()
            {
                var model = StreamCompressionModel.Default;
                var writer = new DataStreamWriter(0, Allocator.Temp);
                var reader = new DataStreamReader(writer.AsNativeArray());
                LogAssert.Expect(LogType.Error, "Trying to read 2 bits from a stream where only 0 are available");
                Assert.That(reader.ReadPackedUInt(model), Is.EqualTo(0));
                Assert.That(reader.HasFailedReads);
            }


            // We test with a variable amount of writes to ensure we overflow the internal ulong bitBuffer.
            [Test]
            public void Flush_Works([Values(1, 7, 24, 65)]int numWrites)
            {
                const int baseline = 100;
                var model = StreamCompressionModel.Default;
                var writer = new DataStreamWriter(512, Allocator.Temp);

                // Writer:
                for(int i = 0; i < numWrites; i++)
                    writer.WritePackedUIntDelta((uint) (100 + i), baseline, model);

                // Flush, and write another sentinel value in.
                writer.Flush();
                writer.WritePackedUIntDelta(45, baseline, model);
                writer.Flush();
                Assert.IsFalse(writer.HasFailedWrites, "Sanity: writer.HasFailedWrites");

                // Reader:
                var reader = new DataStreamReader(writer.AsNativeArray());
                for(int i = 0; i < numWrites; i++)
                {
                    var read = reader.ReadPackedUIntDelta(baseline, model);
                    Assert.AreEqual((uint)(100 + i), read, "100 + i");
                }

                // When we flush the reader, we expect to now be aligned.
                reader.Flush();
                var read45 = reader.ReadPackedUIntDelta(baseline, model);
                Assert.AreEqual(45, read45, "45");
                reader.Flush();
                Assert.IsFalse(reader.HasFailedReads, "reader.HasFailedReads");
                Assert.AreEqual(writer.LengthInBits, reader.GetBitsRead(), "writer.LengthInBits vs reader.GetBitsRead()");
                LogAssert.NoUnexpectedReceived();
            }

            [Test]
            public void NotMirroringFlushCall_Fails()
            {
                const int baseline = 100;
                var model = StreamCompressionModel.Default;
                var writer = new DataStreamWriter(512, Allocator.Temp);

                // Writer:
                writer.WritePackedUIntDelta(100, baseline, model);
                Assert.AreNotEqual(0, writer.LengthInBits % 8, "Sanity: Not byte aligned!");
                writer.Flush();
                writer.WritePackedUIntDelta(45, baseline, model);
                writer.Flush();
                Assert.IsFalse(writer.HasFailedWrites, "Sanity: writer.HasFailedWrites");

                // Reader:
                var reader = new DataStreamReader(writer.AsNativeArray());
                var read = reader.ReadPackedUIntDelta(baseline, model);
                Assert.AreEqual(100u, read, "100u");

                // SKIPPED: reader.Flush();
                // THUS: Read pointer alignment should be wrong.
                LogAssert.ignoreFailingMessages = true; // Defensive: Technically we're reading too FEW bits,
                                                        // so we shouldn't get error logs here.
                Assert.AreNotEqual(45, reader.ReadPackedIntDelta(baseline, model), "!45");
                Assert.AreNotEqual(writer.LengthInBits, reader.GetBitsRead(), "writer.LengthInBits vs reader.GetBitsRead()");
            }

            [Test]
            public void CanMixPackedAndNonPacked()
            {
                const int baseline = 200;
                var model = StreamCompressionModel.Default;
                var writer = new DataStreamWriter(12, Allocator.Temp);

                writer.WritePackedIntDelta(201, baseline, model);
                Assert.AreNotEqual(0, writer.LengthInBits % 8, "Sanity: This test is invalid if we accidentally byte-align.");
                writer.WriteInt(202);
                writer.WritePackedInt(203, model);
                writer.WriteInt(204);

                Assert.IsFalse(writer.HasFailedWrites, "Sanity: writer.HasFailedWrites");

                var reader = new DataStreamReader(writer.AsNativeArray());
                Assert.AreEqual(201, reader.ReadPackedIntDelta(baseline, model));
                Assert.AreEqual(202, reader.ReadInt());
                Assert.AreEqual(203, reader.ReadPackedInt(model));
                Assert.AreEqual(204, reader.ReadInt());
                Assert.IsFalse(reader.HasFailedReads, "reader.HasFailedReads");
            }
        }

        [Test]
        public void IsCreated_ReturnsTrueAfterConstructor()
        {
            var dataStream = new DataStreamWriter(4, Allocator.Temp);
            Assert.True(dataStream.IsCreated, "Buffer must be created after calling constructor.");
        }

        [Test]
        public void LengthInBits_MatchesWrittenCount()
        {
            var dataStream = new DataStreamWriter(4, Allocator.Temp);
            dataStream.WriteByte(0);
            Assert.That(dataStream.LengthInBits, Is.EqualTo(1 * 8));
            dataStream.WriteByte(1);
            dataStream.WriteByte(1);
            Assert.That(dataStream.LengthInBits, Is.EqualTo(3 * 8));
        }

        [Test]
        public void CreateStreamWithPartOfSourceByteArray()
        {
            byte[] byteArray =
            {
                (byte)'s', (byte)'o', (byte)'m', (byte)'e',
                (byte)' ', (byte)'d', (byte)'a', (byte)'t', (byte)'a'
            };

            DataStreamWriter dataStream;
            dataStream = new DataStreamWriter(4, Allocator.Temp);
            dataStream.WriteBytes(new NativeArray<byte>(byteArray, Allocator.Temp).GetSubArray(0, 4));
            Assert.AreEqual(dataStream.Length, 4);
            var reader = new DataStreamReader(dataStream.AsNativeArray());
            for (int i = 0; i < dataStream.Length; ++i)
            {
                Assert.AreEqual(byteArray[i], reader.ReadByte());
            }

#if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG
            LogAssert.Expect(LogType.Error, "Trying to read 1 bytes from a stream where only 0 are available");
            Assert.AreEqual(0, reader.ReadByte());
#endif
        }

        [Test]
        public void CreateStreamWithSourceByteArray()
        {
            byte[] byteArray = new byte[100];
            byteArray[0] = (byte)'a';
            byteArray[1] = (byte)'b';
            byteArray[2] = (byte)'c';

            DataStreamWriter dataStream1, dataStream2;
            dataStream1 = new DataStreamWriter(byteArray.Length, Allocator.Temp);
            dataStream2 = new DataStreamWriter(byteArray.Length, Allocator.Temp);
            dataStream1.WriteBytes(new NativeArray<byte>(byteArray, Allocator.Temp));
            dataStream2.WriteBytes(byteArray);

            var arr1 = dataStream1.AsNativeArray();
            var arr2 = dataStream2.AsNativeArray();
            var reader1 = new DataStreamReader(arr1);
            var reader2 = new DataStreamReader(arr2);
            for (var i = 0; i < byteArray.Length; ++i)
            {
                Assert.AreEqual(byteArray[i], reader1.ReadByte());
                Assert.AreEqual(byteArray[i], reader2.ReadByte());
            }
        }

        [Test]
        public void ReadIntoExistingNativeByteArray()
        {
            var byteArray = new NativeArray<byte>(100, Allocator.Temp);

            DataStreamWriter dataStream;
            dataStream = new DataStreamWriter(3, Allocator.Temp);
            {
                dataStream.WriteByte((byte)'a');
                dataStream.WriteByte((byte)'b');
                dataStream.WriteByte((byte)'c');
                var reader = new DataStreamReader(dataStream.AsNativeArray());
                reader.ReadBytes(byteArray.GetSubArray(0, dataStream.Length));
                reader = new DataStreamReader(dataStream.AsNativeArray());
                for (int i = 0; i < reader.Length; ++i)
                {
                    Assert.AreEqual(byteArray[i], reader.ReadByte());
                }
            }
        }

        [Test]
        public void ReadIntoExistingByteArray()
        {
            var byteArray = new byte[3];

            DataStreamWriter dataStream;
            dataStream = new DataStreamWriter(3, Allocator.Temp);
            {
                dataStream.WriteByte((byte)'a');
                dataStream.WriteByte((byte)'b');
                dataStream.WriteByte((byte)'c');
                var reader = new DataStreamReader(dataStream.AsNativeArray());
                reader.ReadBytes(byteArray);
                reader = new DataStreamReader(dataStream.AsNativeArray());
                for (int i = 0; i < reader.Length; ++i)
                {
                    Assert.AreEqual(byteArray[i], reader.ReadByte());
                }
            }
        }

        [Test]
        public void ReadingDataFromStreamWithSliceOffset()
        {
            var dataStream = new DataStreamWriter(100, Allocator.Temp);
            dataStream.WriteByte((byte)'a');
            dataStream.WriteByte((byte)'b');
            dataStream.WriteByte((byte)'c');
            dataStream.WriteByte((byte)'d');
            dataStream.WriteByte((byte)'e');
            dataStream.WriteByte((byte)'f');
            var reader = new DataStreamReader(dataStream.AsNativeArray().GetSubArray(3, 3));
            Assert.AreEqual('d', reader.ReadByte());
            Assert.AreEqual('e', reader.ReadByte());
            Assert.AreEqual('f', reader.ReadByte());
        }

        [Test]
        public void GetStreamReaderUnsafePtr()
        {
            var a = new NativeArray<byte>(1, Allocator.Temp);
            a[0] = (byte)42;
            var reader = new DataStreamReader(a);

            unsafe
            {
                var ptr = (byte*)reader.GetUnsafeReadOnlyPtr();
                Assert.AreEqual((byte)42, *ptr);
            }
        }

        [Test]
        public void WriteOutOfBounds()
        {
            var dataStream = new DataStreamWriter(9, Allocator.Temp);
            Assert.IsTrue(dataStream.WriteInt(42));
            Assert.AreEqual(4, dataStream.Length);
            Assert.IsTrue(dataStream.WriteInt(42));
            Assert.AreEqual(8, dataStream.Length);
            Assert.IsFalse(dataStream.HasFailedWrites);
            Assert.IsFalse(dataStream.WriteInt(42));
            Assert.AreEqual(8, dataStream.Length);
            Assert.IsTrue(dataStream.HasFailedWrites);

            Assert.IsFalse(dataStream.WriteShort(42));
            Assert.AreEqual(8, dataStream.Length);
            Assert.IsTrue(dataStream.HasFailedWrites);

            Assert.IsTrue(dataStream.WriteByte(42));
            Assert.AreEqual(9, dataStream.Length);
            Assert.IsTrue(dataStream.HasFailedWrites);

            Assert.IsFalse(dataStream.WriteByte(42));
            Assert.AreEqual(9, dataStream.Length);
            Assert.IsTrue(dataStream.HasFailedWrites);
        }

        [Test]
        public void ReadWritePackedUIntWithDeferred()
        {
            var compressionModel = StreamCompressionModel.Default;
            var dataStream = new DataStreamWriter(300 * 4, Allocator.Temp);
            uint base_val = 2000;
            uint count = 277;
            var def = dataStream;
            dataStream.WriteInt((int)0);
            for (uint i = 0; i < count; ++i)
                dataStream.WritePackedUInt(base_val + i, compressionModel);

            dataStream.Flush();
            def.WriteInt(1979);
            def = dataStream;
            dataStream.WriteInt((int)0);
            def.WriteInt(1979);
            dataStream.Flush();
            var reader = new DataStreamReader(dataStream.AsNativeArray());
            Assert.AreEqual(1979, reader.ReadInt());
            for (uint i = 0; i < count; ++i)
            {
                var val = reader.ReadPackedUInt(compressionModel);
                Assert.AreEqual(base_val + i, val);
            }

            Assert.AreEqual(1979, reader.ReadInt());
        }

        [Test]
        public void PassDataStreamReaderToJob()
        {
            using (var returnValue = new NativeArray<int>(1, Allocator.TempJob))
            {
                var writer = new DataStreamWriter(sizeof(int), Allocator.Temp);
                writer.WriteInt(42);

                var reader = new DataStreamReader(writer.AsNativeArray());

                new ReaderTestJob
                {
                    Reader = reader,
                    ReturnValue = returnValue
                }.Run();

                Assert.AreEqual(42, returnValue[0]);
            }
        }

        private struct ReaderTestJob : IJob
        {
            public DataStreamReader Reader;
            public NativeArray<int> ReturnValue;

            public void Execute()
            {
                ReturnValue[0] = Reader.ReadInt();
            }
        }

        delegate T Read<out T>(ref DataStreamReader reader);
        delegate T ReadPacked<out T>(ref DataStreamReader reader, StreamCompressionModel model);
        delegate bool Write(ref DataStreamWriter writer);
        delegate T Sum<T>(T x, uint y);
        delegate bool WriteWithValue<in T>(ref DataStreamWriter writer, T value);
        delegate bool WritePacked(ref DataStreamWriter writer, StreamCompressionModel model);
        delegate bool WritePackedWithValue<in T>(ref DataStreamWriter writer, StreamCompressionModel model, T value);

        const int FixedStringHeader = 2;

        static void CheckReadWritePackedLooped<T>(int size, T baseVal, T expected,
            Write write, WritePackedWithValue<T> writePackedWithValue,
            Read<T> read, ReadPacked<T> readPacked, Sum<T> sum)
        {
            var compressionModel = StreamCompressionModel.Default;
            var dataStream = new DataStreamWriter(300 * size, Allocator.Temp);
            const int count = 277;
            for (uint i = 0; i < count; ++i)
            {
                T res = sum(baseVal, i);
                writePackedWithValue(ref dataStream, compressionModel, res);
            }

            write(ref dataStream);
            dataStream.Flush();
            var reader = new DataStreamReader(dataStream.AsNativeArray());
            for (uint i = 0; i < count; ++i)
            {
                var val = readPacked(ref reader, compressionModel);
                Assert.AreEqual(sum(baseVal, i), val);
            }

            Assert.AreEqual(expected, read(ref reader));
        }

        static void CheckReadWritePacked<T>(int size, ReadPacked<T> read, WritePacked write, T value)
        {
            var model = StreamCompressionModel.Default;
            var writer = new DataStreamWriter(size, Allocator.Temp);
            write(ref writer, model);
            writer.Flush();
            var reader = new DataStreamReader(writer.AsNativeArray());
            Assert.That(read(ref reader, model), Is.EqualTo(value));
        }

        static void CheckReadWrite<T>(int size, Func<DataStreamReader, T> read, Write write, T value)
        {
            var writer = new DataStreamWriter(size, Allocator.Temp);
            write(ref writer);
            writer.Flush();
            Assert.That(read(new DataStreamReader(writer.AsNativeArray())), Is.EqualTo(value));
        }

        static void CheckReadWriteLooped<T>(int size, T baseVal, T expected, WriteWithValue<T> write, Read<T> read, Sum<T> sum)
        {
            var writer = new DataStreamWriter(300 * size, Allocator.Temp);
            const int count = 277;
            for (uint i = 0; i < count; ++i)
            {
                write(ref writer, sum(baseVal, i));
            }

            write(ref writer, expected);
            writer.Flush();
            var reader = new DataStreamReader(writer.AsNativeArray());
            for (uint i = 0; i < count; ++i)
            {
                var val = read(ref reader);
                Assert.AreEqual(sum(baseVal, i), val);
            }

            Assert.AreEqual(expected, read(ref reader));
        }

        [Test]
        public void MiNiCheck()
        {
            var model = StreamCompressionModel.Default;
            Assert.That(model.ToString(), Is.EqualTo("Unity.Collections.StreamCompressionModel"));
        }
    }
}