/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2017-2018 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#include "serialiser.h"

#if ENABLED(ENABLE_UNIT_TESTS)

#include "3rdparty/catch/catch.hpp"

void WriteAllBasicTypes(WriteSerialiser &ser)
{
  int64_t a = -1;
  uint64_t b = 2;
  int32_t c = -3;
  uint32_t d = 4;
  int16_t e = -5;
  uint16_t f = 6;
  int8_t g = -7;
  uint8_t h = 8;

  bool i = true;

  char j = 'j';

  double k = 11.11011011;
  float l = 12.12012012f;

  std::string m = "mmmm";
  char n[5] = "nnnn";
  const char *s = "ssss";

  int t[4] = {20, 20, 20, 20};

  SERIALISE_ELEMENT(a);
  SERIALISE_ELEMENT(b);
  SERIALISE_ELEMENT(c);
  SERIALISE_ELEMENT(d);
  SERIALISE_ELEMENT(e);
  SERIALISE_ELEMENT(f);
  SERIALISE_ELEMENT(g);
  SERIALISE_ELEMENT(h);
  SERIALISE_ELEMENT(i);
  SERIALISE_ELEMENT(j);
  SERIALISE_ELEMENT(k);
  SERIALISE_ELEMENT(l);
  SERIALISE_ELEMENT(m);
  SERIALISE_ELEMENT(n);
  SERIALISE_ELEMENT(s);
  SERIALISE_ELEMENT(t);
}

TEST_CASE("Read/write basic types", "[serialiser][structured]")
{
  StreamWriter *buf = new StreamWriter(StreamWriter::DefaultScratchSize);

  // write basic types, verify that we didn't write too much (rough factor of total data size +
  // overhead - it's OK to update this value if serialisation changed as long as it's incremental).
  {
    WriteSerialiser ser(buf, Ownership::Nothing);

    {
      SCOPED_SERIALISE_CHUNK(5);

      WriteAllBasicTypes(ser);
    }

    CHECK(buf->GetOffset() <= 128);

    REQUIRE_FALSE(ser.IsErrored());
  }

  {
    ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);

    uint32_t chunkID = ser.ReadChunk<uint32_t>();

    CHECK(chunkID == 5);

    int64_t a;
    uint64_t b;
    int32_t c;
    uint32_t d;
    int16_t e;
    uint16_t f;
    int8_t g;
    uint8_t h;

    bool i;

    char j;

    double k;
    float l;

    std::string m;
    char n[5];
    const char *s;

    int t[4] = {0, 0, 0, 0};

    SERIALISE_ELEMENT(a);
    SERIALISE_ELEMENT(b);
    SERIALISE_ELEMENT(c);
    SERIALISE_ELEMENT(d);
    SERIALISE_ELEMENT(e);
    SERIALISE_ELEMENT(f);
    SERIALISE_ELEMENT(g);
    SERIALISE_ELEMENT(h);
    SERIALISE_ELEMENT(i);
    SERIALISE_ELEMENT(j);
    SERIALISE_ELEMENT(k);
    SERIALISE_ELEMENT(l);
    SERIALISE_ELEMENT(m);
    SERIALISE_ELEMENT(n);
    SERIALISE_ELEMENT(s);
    SERIALISE_ELEMENT(t);

    ser.EndChunk();

    REQUIRE_FALSE(ser.IsErrored());

    CHECK(ser.GetReader()->AtEnd());

    CHECK(a == -1);
    CHECK(b == 2);
    CHECK(c == -3);
    CHECK(d == 4);
    CHECK(e == -5);
    CHECK(f == 6);
    CHECK(g == -7);
    CHECK(h == 8);

    CHECK(i == true);

    CHECK(j == 'j');

    CHECK(k == 11.11011011);
    CHECK(l == 12.12012012f);

    CHECK(m == "mmmm");
    CHECK(std::string(n) == "nnnn");
    CHECK(std::string(s) == "ssss");

    CHECK(t[0] == 20);
    CHECK(t[1] == 20);
    CHECK(t[2] == 20);
    CHECK(t[3] == 20);
  }

  delete buf;
};

TEST_CASE("Read/write via structured of basic types", "[serialiser]")
{
  StreamWriter *buf = new StreamWriter(StreamWriter::DefaultScratchSize);

  // write basic types, verify that we didn't write too much (rough factor of total data size +
  // overhead - it's OK to update this value if serialisation changed as long as it's incremental).
  {
    WriteSerialiser ser(buf, Ownership::Nothing);
    ser.WriteChunk(5);
    WriteAllBasicTypes(ser);
    ser.EndChunk();
  }

  {
    ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);

    ChunkLookup testChunkLoop = [](uint32_t) -> std::string { return "TestChunk"; };

    ser.ConfigureStructuredExport(testChunkLoop, true);

    int64_t a;
    uint64_t b;
    int32_t c;
    uint32_t d;
    int16_t e;
    uint16_t f;
    int8_t g;
    uint8_t h;

    bool i;

    char j;

    double k;
    float l;

    std::string m;
    char n[5];
    const char *s;

    int t[4];

    ser.ReadChunk<uint32_t>();

    SERIALISE_ELEMENT(a);
    SERIALISE_ELEMENT(b);
    SERIALISE_ELEMENT(c);
    SERIALISE_ELEMENT(d);
    SERIALISE_ELEMENT(e);
    SERIALISE_ELEMENT(f);
    SERIALISE_ELEMENT(g);
    SERIALISE_ELEMENT(h);
    SERIALISE_ELEMENT(i);
    SERIALISE_ELEMENT(j);
    SERIALISE_ELEMENT(k);
    SERIALISE_ELEMENT(l);
    SERIALISE_ELEMENT(m);
    SERIALISE_ELEMENT(n);
    SERIALISE_ELEMENT(s);
    SERIALISE_ELEMENT(t);

    ser.EndChunk();

    REQUIRE_FALSE(ser.IsErrored());

    CHECK(ser.GetReader()->AtEnd());

    const SDFile &structFile = ser.GetStructuredFile();

    CHECK(structFile.chunks.size() == 1);
    CHECK(structFile.buffers.size() == 0);

    REQUIRE(structFile.chunks[0]);

    const SDChunk &chunk = *structFile.chunks[0];

    CHECK(chunk.name == testChunkLoop(0));
    CHECK(chunk.metadata.chunkID == 5);
    CHECK(chunk.metadata.length == chunk.type.byteSize);
    CHECK(chunk.metadata.length < ser.GetReader()->GetSize());
    CHECK(chunk.type.basetype == SDBasic::Chunk);
    CHECK(chunk.type.name == "Chunk");
    CHECK(chunk.data.children.size() == 16);

    for(SDObject *o : chunk.data.children)
      REQUIRE(o);

    int childIdx = 0;

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "a");
      CHECK(o.type.name == "int64_t");
      CHECK(o.type.basetype == SDBasic::SignedInteger);
      CHECK(o.type.byteSize == 8);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.i == -1);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "b");
      CHECK(o.type.name == "uint64_t");
      CHECK(o.type.basetype == SDBasic::UnsignedInteger);
      CHECK(o.type.byteSize == 8);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.u == 2);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "c");
      CHECK(o.type.name == "int32_t");
      CHECK(o.type.basetype == SDBasic::SignedInteger);
      CHECK(o.type.byteSize == 4);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.i == -3);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "d");
      CHECK(o.type.name == "uint32_t");
      CHECK(o.type.basetype == SDBasic::UnsignedInteger);
      CHECK(o.type.byteSize == 4);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.u == 4);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "e");
      CHECK(o.type.name == "int16_t");
      CHECK(o.type.basetype == SDBasic::SignedInteger);
      CHECK(o.type.byteSize == 2);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.i == -5);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "f");
      CHECK(o.type.name == "uint16_t");
      CHECK(o.type.basetype == SDBasic::UnsignedInteger);
      CHECK(o.type.byteSize == 2);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.u == 6);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "g");
      CHECK(o.type.name == "int8_t");
      CHECK(o.type.basetype == SDBasic::SignedInteger);
      CHECK(o.type.byteSize == 1);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.i == -7);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "h");
      CHECK(o.type.name == "uint8_t");
      CHECK(o.type.basetype == SDBasic::UnsignedInteger);
      CHECK(o.type.byteSize == 1);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.u == 8);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "i");
      CHECK(o.type.name == "bool");
      CHECK(o.type.basetype == SDBasic::Boolean);
      CHECK(o.type.byteSize == 1);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.b == true);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "j");
      CHECK(o.type.name == "char");
      CHECK(o.type.basetype == SDBasic::Character);
      CHECK(o.type.byteSize == 1);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.c == 'j');
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "k");
      CHECK(o.type.name == "double");
      CHECK(o.type.basetype == SDBasic::Float);
      CHECK(o.type.byteSize == 8);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.d == 11.11011011);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "l");
      CHECK(o.type.name == "float");
      CHECK(o.type.basetype == SDBasic::Float);
      CHECK(o.type.byteSize == 4);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.basic.d == Approx(12.12012012));
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "m");
      CHECK(o.type.name == "string");
      CHECK(o.type.basetype == SDBasic::String);
      CHECK(o.type.byteSize == 4);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.str == "mmmm");
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "n");
      CHECK(o.type.name == "string");
      CHECK(o.type.basetype == SDBasic::String);
      CHECK(o.type.byteSize == 4);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.str == "nnnn");
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "s");
      CHECK(o.type.name == "string");
      CHECK(o.type.basetype == SDBasic::String);
      CHECK(o.type.byteSize == 4);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);

      CHECK(o.data.str == "ssss");
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "t");
      CHECK(o.type.name == "int32_t");
      CHECK(o.type.basetype == SDBasic::Array);
      CHECK(o.type.byteSize == 4);
      CHECK(o.type.flags == SDTypeFlags::FixedArray);

      CHECK(o.data.children.size() == 4);

      CHECK(o.data.children[0]->data.basic.i == 20);
      CHECK(o.data.children[1]->data.basic.c == 20);
      CHECK(o.data.children[2]->data.basic.c == 20);
      CHECK(o.data.children[3]->data.basic.c == 20);
    }

    StreamWriter *rewriteBuf = new StreamWriter(StreamWriter::DefaultScratchSize);

    {
      WriteSerialiser rewrite(rewriteBuf, Ownership::Nothing);

      rewrite.WriteStructuredFile(structFile, NULL);
    }

    // must be bitwise identical to the original serialised data.
    REQUIRE(rewriteBuf->GetOffset() == buf->GetOffset());
    CHECK_FALSE(memcmp(rewriteBuf->GetData(), buf->GetData(), (size_t)rewriteBuf->GetOffset()));

    delete rewriteBuf;
  }

  delete buf;
};

TEST_CASE("Read/write chunk metadata", "[serialiser]")
{
  StreamWriter *buf = new StreamWriter(StreamWriter::DefaultScratchSize);

  // write basic types, verify that we didn't write too much (rough factor of total data size +
  // overhead - it's OK to update this value if serialisation changed as long as it's incremental).
  {
    WriteSerialiser ser(buf, Ownership::Nothing);

    ser.SetChunkMetadataRecording(WriteSerialiser::ChunkCallstack | WriteSerialiser::ChunkDuration |
                                  WriteSerialiser::ChunkThreadID | WriteSerialiser::ChunkTimestamp);

    ser.ChunkMetadata().threadID = 12345ULL;
    ser.ChunkMetadata().durationMicro = 445566ULL;
    ser.ChunkMetadata().timestampMicro = 987654321ULL;
    ser.ChunkMetadata().callstack.resize(4);
    ser.ChunkMetadata().callstack[0] = 101;
    ser.ChunkMetadata().callstack[1] = 102;
    ser.ChunkMetadata().callstack[2] = 103;
    ser.ChunkMetadata().callstack[3] = 104;

    ser.WriteChunk(1);

    uint32_t dummy = 99;
    ser.Serialise("dummy", dummy);

    ser.EndChunk();

    REQUIRE_FALSE(ser.IsErrored());
  }

  {
    ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);

    ser.ReadChunk<uint32_t>();

    CHECK(ser.ChunkMetadata().threadID == 12345ULL);
    CHECK(ser.ChunkMetadata().durationMicro == 445566ULL);
    CHECK(ser.ChunkMetadata().timestampMicro == 987654321ULL);
    REQUIRE(ser.ChunkMetadata().callstack.size() == 4);
    CHECK(ser.ChunkMetadata().callstack[0] == 101);
    CHECK(ser.ChunkMetadata().callstack[1] == 102);
    CHECK(ser.ChunkMetadata().callstack[2] == 103);
    CHECK(ser.ChunkMetadata().callstack[3] == 104);

    ser.SkipCurrentChunk();

    ser.EndChunk();

    REQUIRE_FALSE(ser.IsErrored());

    CHECK(ser.GetReader()->AtEnd());
  }

  {
    ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);

    ChunkLookup testChunkLoop = [](uint32_t) -> std::string { return "TestChunk"; };

    ser.ConfigureStructuredExport(testChunkLoop, true);

    ser.ReadChunk<uint32_t>();

    uint32_t dummy;
    ser.Serialise("dummy", dummy);

    ser.EndChunk();

    SDChunkMetaData &md = ser.GetStructuredFile().chunks[0]->metadata;

    CHECK(md.threadID == 12345ULL);
    CHECK(md.durationMicro == 445566ULL);
    CHECK(md.timestampMicro == 987654321ULL);
    REQUIRE(md.callstack.size() == 4);
    CHECK(md.callstack[0] == 101);
    CHECK(md.callstack[1] == 102);
    CHECK(md.callstack[2] == 103);
    CHECK(md.callstack[3] == 104);

    REQUIRE_FALSE(ser.IsErrored());

    CHECK(ser.GetReader()->AtEnd());
  }

  delete buf;
};

TEST_CASE("Verify multiple chunks can be merged", "[serialiser][chunks]")
{
  StreamWriter *buf = new StreamWriter(StreamWriter::DefaultScratchSize);

  enum ChunkType
  {
    FLOAT4 = 5,
    TWO_INTS,
    BOOL_INT_FLOAT,
    STRING_AND_INT,
  };

  // write some chunks individually
  std::vector<Chunk *> chunks;
  {
    WriteSerialiser ser(new StreamWriter(StreamWriter::DefaultScratchSize), Ownership::Stream);

    {
      SCOPED_SERIALISE_CHUNK(TWO_INTS);

      int first = 123;
      int second = 456;

      SERIALISE_ELEMENT(first);
      SERIALISE_ELEMENT(second);

      chunks.push_back(scope.Get());
      REQUIRE(chunks.back());
    }

    {
      SCOPED_SERIALISE_CHUNK(STRING_AND_INT);

      string s = "string in STRING_AND_INT";
      int i = 4096;

      SERIALISE_ELEMENT(s);
      SERIALISE_ELEMENT(i);

      chunks.push_back(scope.Get());
      REQUIRE(chunks.back());
    }

    REQUIRE_FALSE(ser.IsErrored());
    REQUIRE(chunks.size() == 2);
  }

  // now write the previous chunks, then some more in-line
  {
    WriteSerialiser ser(buf, Ownership::Nothing);

    for(Chunk *c : chunks)
      c->Write(ser);

    {
      SCOPED_SERIALISE_CHUNK(BOOL_INT_FLOAT);

      bool flag = false;
      int data = 10000;
      float value = 3.141592f;

      SERIALISE_ELEMENT(flag);
      SERIALISE_ELEMENT(data);
      SERIALISE_ELEMENT(value);
    }

    {
      SCOPED_SERIALISE_CHUNK(FLOAT4);

      float vec4[4] = {1.1f, 2.2f, 3.3f, 4.4f};

      SERIALISE_ELEMENT(vec4);
    }

    REQUIRE_FALSE(ser.IsErrored());
    CHECK(buf->GetOffset() <= 256);
  }

  for(Chunk *c : chunks)
    delete c;

  // now read the data "dynamically" and ensure it's all correct
  {
    ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);

    while(!ser.GetReader()->AtEnd())
    {
      uint32_t chunkID = ser.ReadChunk<uint32_t>();
      ChunkType chunk = (ChunkType)chunkID;

      switch(chunk)
      {
        case FLOAT4:
        {
          float vec4[4];

          SERIALISE_ELEMENT(vec4);

          CHECK(vec4[0] == 1.1f);
          CHECK(vec4[1] == 2.2f);
          CHECK(vec4[2] == 3.3f);
          CHECK(vec4[3] == 4.4f);
          break;
        }
        case TWO_INTS:
        {
          int first = 0;
          int second = 0;

          SERIALISE_ELEMENT(first);
          SERIALISE_ELEMENT(second);

          CHECK(first == 123);
          CHECK(second == 456);
          break;
        }
        case BOOL_INT_FLOAT:
        {
          bool flag = true;
          int data = 0;
          float value = 0.0f;

          SERIALISE_ELEMENT(flag);
          SERIALISE_ELEMENT(data);
          SERIALISE_ELEMENT(value);

          CHECK(flag == false);
          CHECK(data == 10000);
          CHECK(value == 3.141592f);
          break;
        }
        case STRING_AND_INT:
        {
          string s;
          int i = 0;

          SERIALISE_ELEMENT(s);
          SERIALISE_ELEMENT(i);

          CHECK(s == "string in STRING_AND_INT");
          CHECK(i == 4096);
          break;
        }
        default:
        {
          CAPTURE(chunkID);
          FAIL("Unexpected chunk type");
          break;
        }
      }

      ser.EndChunk();
    }
  }

  delete buf;
};

TEST_CASE("Read/write container types", "[serialiser][structured]")
{
  StreamWriter *buf = new StreamWriter(StreamWriter::DefaultScratchSize);

  // write basic types, verify that we didn't write too much (rough factor of total data size +
  // overhead - it's OK to update this value if serialisation changed as long as it's incremental).
  {
    WriteSerialiser ser(buf, Ownership::Nothing);

    {
      SCOPED_SERIALISE_CHUNK(5);

      std::vector<int> v;
      std::pair<float, string> p;
      std::list<uint16_t> l;

      v.push_back(1);
      v.push_back(1);
      v.push_back(2);
      v.push_back(3);
      v.push_back(5);
      v.push_back(8);

      p = std::make_pair(3.14159f, "M_PI");

      l.push_back(2U);
      l.push_back(3U);
      l.push_back(5U);
      l.push_back(7U);
      l.push_back(11U);
      l.push_back(13U);

      SERIALISE_ELEMENT(v);
      SERIALISE_ELEMENT(p);
      SERIALISE_ELEMENT(l);
    }

    CHECK(buf->GetOffset() <= 128);

    REQUIRE_FALSE(ser.IsErrored());
  }

  {
    ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);

    uint32_t chunkID = ser.ReadChunk<uint32_t>();

    CHECK(chunkID == 5);

    std::vector<int> v;
    std::pair<float, string> p;
    std::list<uint16_t> l;

    SERIALISE_ELEMENT(v);
    SERIALISE_ELEMENT(p);
    SERIALISE_ELEMENT(l);

    ser.EndChunk();

    REQUIRE_FALSE(ser.IsErrored());

    CHECK(ser.GetReader()->AtEnd());

    REQUIRE(v.size() == 6);
    REQUIRE(l.size() == 6);

    CHECK(v[0] == 1);
    CHECK(v[1] == 1);
    CHECK(v[2] == 2);
    CHECK(v[3] == 3);
    CHECK(v[4] == 5);
    CHECK(v[5] == 8);

    CHECK(p.first == 3.14159f);
    CHECK(p.second == "M_PI");

    auto it = l.begin();

    CHECK(*it == 2U);
    ++it;
    CHECK(*it == 3U);
    ++it;
    CHECK(*it == 5U);
    ++it;
    CHECK(*it == 7U);
    ++it;
    CHECK(*it == 11U);
    ++it;
    CHECK(*it == 13U);
  }

  {
    ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);

    ser.ConfigureStructuredExport([](uint32_t) -> std::string { return "TestChunk"; }, true);

    ser.ReadChunk<uint32_t>();
    {
      std::vector<int32_t> v;
      std::pair<float, string> p;
      std::list<uint16_t> l;

      SERIALISE_ELEMENT(v);
      SERIALISE_ELEMENT(p);
      SERIALISE_ELEMENT(l);
    }
    ser.EndChunk();

    REQUIRE_FALSE(ser.IsErrored());

    CHECK(ser.GetReader()->AtEnd());

    const SDFile &structData = ser.GetStructuredFile();

    CHECK(structData.chunks.size() == 1);
    CHECK(structData.buffers.size() == 0);

    REQUIRE(structData.chunks[0]);

    const SDChunk &chunk = *structData.chunks[0];

    CHECK(chunk.data.children.size() == 3);

    for(SDObject *o : chunk.data.children)
      REQUIRE(o);

    int childIdx = 0;

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "v");
      CHECK(o.type.basetype == SDBasic::Array);
      CHECK(o.type.byteSize == 6);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);
      CHECK(o.data.children.size() == 6);

      for(SDObject *child : o.data.children)
      {
        CHECK(child->type.basetype == SDBasic::SignedInteger);
        CHECK(child->type.byteSize == 4);
      }

      CHECK(o.data.children[0]->data.basic.i == 1);
      CHECK(o.data.children[1]->data.basic.i == 1);
      CHECK(o.data.children[2]->data.basic.i == 2);
      CHECK(o.data.children[3]->data.basic.i == 3);
      CHECK(o.data.children[4]->data.basic.i == 5);
      CHECK(o.data.children[5]->data.basic.i == 8);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "p");
      CHECK(o.type.name == "pair");
      CHECK(o.type.basetype == SDBasic::Struct);
      CHECK(o.type.byteSize == 2);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);
      CHECK(o.data.children.size() == 2);

      {
        SDObject &first = *o.data.children[0];

        CHECK(first.name == "first");
        CHECK(first.type.name == "float");
        CHECK(first.type.basetype == SDBasic::Float);
        CHECK(first.type.byteSize == 4);
        CHECK(first.type.flags == SDTypeFlags::NoFlags);

        CHECK(first.data.basic.d == 3.14159f);
      }

      {
        SDObject &second = *o.data.children[1];

        CHECK(second.name == "second");
        CHECK(second.type.name == "string");
        CHECK(second.type.basetype == SDBasic::String);
        CHECK(second.type.byteSize == 4);
        CHECK(second.type.flags == SDTypeFlags::NoFlags);

        CHECK(second.data.str == "M_PI");
      }
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "l");
      CHECK(o.type.basetype == SDBasic::Array);
      CHECK(o.type.byteSize == 6);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);
      CHECK(o.data.children.size() == 6);

      for(SDObject *child : o.data.children)
      {
        CHECK(child->type.basetype == SDBasic::UnsignedInteger);
        CHECK(child->type.byteSize == 2);
      }

      CHECK(o.data.children[0]->data.basic.u == 2U);
      CHECK(o.data.children[1]->data.basic.u == 3U);
      CHECK(o.data.children[2]->data.basic.u == 5U);
      CHECK(o.data.children[3]->data.basic.u == 7U);
      CHECK(o.data.children[4]->data.basic.u == 11U);
      CHECK(o.data.children[5]->data.basic.u == 13U);
    }

    StreamWriter *rewriteBuf = new StreamWriter(StreamWriter::DefaultScratchSize);

    {
      WriteSerialiser rewrite(rewriteBuf, Ownership::Nothing);

      rewrite.WriteStructuredFile(structData, NULL);
    }

    // must be bitwise identical to the original serialised data.
    REQUIRE(rewriteBuf->GetOffset() == buf->GetOffset());
    CHECK_FALSE(memcmp(rewriteBuf->GetData(), buf->GetData(), (size_t)rewriteBuf->GetOffset()));

    delete rewriteBuf;
  }

  delete buf;
};

struct struct1
{
  struct1() : x(0.0f), y(0.0f), width(0.0f), height(0.0f) {}
  struct1(float X, float Y, float W, float H) : x(X), y(Y), width(W), height(H) {}
  float x, y, width, height;
};

DECLARE_REFLECTION_STRUCT(struct1);

template <class SerialiserType>
void DoSerialise(SerialiserType &ser, struct1 &el)
{
  SERIALISE_MEMBER(x);
  SERIALISE_MEMBER(y);
  SERIALISE_MEMBER(width);
  SERIALISE_MEMBER(height);
}

struct struct2
{
  string name;
  std::vector<float> floats;
  std::vector<struct1> viewports;
};

DECLARE_REFLECTION_STRUCT(struct2);

template <class SerialiserType>
void DoSerialise(SerialiserType &ser, struct2 &el)
{
  SERIALISE_MEMBER(name);
  SERIALISE_MEMBER(floats);
  SERIALISE_MEMBER(viewports);
}

enum MySpecialEnum
{
  FirstEnumValue,
  SecondEnumValue,
  AnotherEnumValue,
  TheLastEnumValue,
};

DECLARE_REFLECTION_ENUM(MySpecialEnum);

template <>
std::string DoStringise(const MySpecialEnum &el)
{
  BEGIN_ENUM_STRINGISE(MySpecialEnum);
  {
    STRINGISE_ENUM(FirstEnumValue);
    STRINGISE_ENUM(SecondEnumValue);
    STRINGISE_ENUM(AnotherEnumValue);
    STRINGISE_ENUM(TheLastEnumValue);
  }
  END_ENUM_STRINGISE();
}

TEST_CASE("Read/write complex types", "[serialiser][structured]")
{
  StreamWriter *buf = new StreamWriter(StreamWriter::DefaultScratchSize);

  {
    WriteSerialiser ser(buf, Ownership::Nothing);

    SCOPED_SERIALISE_CHUNK(5);

    MySpecialEnum enumVal = AnotherEnumValue;

    SERIALISE_ELEMENT(enumVal);

    std::vector<struct1> sparseStructArray;

    sparseStructArray.resize(10);

    sparseStructArray[5] = struct1(1.0f, 2.0f, 3.0f, 4.0f);
    sparseStructArray[8] = struct1(10.0f, 20.0f, 30.0f, 40.0f);

    SERIALISE_ELEMENT(sparseStructArray);

    struct2 complex;
    complex.name = "A complex object";
    complex.floats = {1.2f, 3.4f, 5.6f};
    complex.viewports.resize(4);
    complex.viewports[0] = struct1(512.0f, 0.0f, 256.0f, 256.0f);

    SERIALISE_ELEMENT(complex);

    const struct1 *inputParam1 = new struct1(9.0f, 9.9f, 9.99f, 9.999f);
    const struct1 *inputParam2 = NULL;

    SERIALISE_ELEMENT_OPT(inputParam1);
    SERIALISE_ELEMENT_OPT(inputParam2);

    CHECK(buf->GetOffset() <= 512);

    REQUIRE_FALSE(ser.IsErrored());

    delete inputParam1;
  }

  {
    ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);

    uint32_t chunkID = ser.ReadChunk<uint32_t>();

    CHECK(chunkID == 5);

    MySpecialEnum enumVal;

    SERIALISE_ELEMENT(enumVal);

    std::vector<struct1> sparseStructArray;

    SERIALISE_ELEMENT(sparseStructArray);

    struct2 complex;

    SERIALISE_ELEMENT(complex);

    const struct1 *inputParam1;
    const struct1 *inputParam2;

    SERIALISE_ELEMENT_OPT(inputParam1);
    SERIALISE_ELEMENT_OPT(inputParam2);

    ser.EndChunk();

    REQUIRE_FALSE(ser.IsErrored());

    CHECK(ser.GetReader()->AtEnd());

    CHECK(enumVal == AnotherEnumValue);

    CHECK(sparseStructArray[0].x == 0.0f);
    CHECK(sparseStructArray[0].y == 0.0f);
    CHECK(sparseStructArray[0].width == 0.0f);
    CHECK(sparseStructArray[0].height == 0.0f);

    CHECK(sparseStructArray[5].x == 1.0f);
    CHECK(sparseStructArray[5].y == 2.0f);
    CHECK(sparseStructArray[5].width == 3.0f);
    CHECK(sparseStructArray[5].height == 4.0f);

    CHECK(sparseStructArray[8].x == 10.0f);
    CHECK(sparseStructArray[8].y == 20.0f);
    CHECK(sparseStructArray[8].width == 30.0f);
    CHECK(sparseStructArray[8].height == 40.0f);

    CHECK(complex.name == "A complex object");
    REQUIRE(complex.floats.size() == 3);
    CHECK(complex.floats[0] == 1.2f);
    CHECK(complex.floats[1] == 3.4f);
    CHECK(complex.floats[2] == 5.6f);
    REQUIRE(complex.viewports.size() == 4);

    CHECK(complex.viewports[0].x == 512.0f);
    CHECK(complex.viewports[0].y == 0.0f);
    CHECK(complex.viewports[0].width == 256.0f);
    CHECK(complex.viewports[0].height == 256.0f);

    CHECK(inputParam1->x == 9.0f);
    CHECK(inputParam1->y == 9.9f);
    CHECK(inputParam1->width == 9.99f);
    CHECK(inputParam1->height == 9.999f);

    CHECK(inputParam2 == NULL);
  }

  {
    ReadSerialiser ser(new StreamReader(buf->GetData(), buf->GetOffset()), Ownership::Stream);

    ser.ConfigureStructuredExport([](uint32_t) -> std::string { return "TestChunk"; }, true);

    ser.ReadChunk<uint32_t>();
    {
      MySpecialEnum enumVal;

      SERIALISE_ELEMENT(enumVal);

      std::vector<struct1> sparseStructArray;

      SERIALISE_ELEMENT(sparseStructArray);

      struct2 complex;

      SERIALISE_ELEMENT(complex);

      const struct1 *inputParam1;
      const struct1 *inputParam2;

      SERIALISE_ELEMENT_OPT(inputParam1);
      SERIALISE_ELEMENT_OPT(inputParam2);
    }
    ser.EndChunk();

    REQUIRE_FALSE(ser.IsErrored());

    CHECK(ser.GetReader()->AtEnd());

    const SDFile &structData = ser.GetStructuredFile();

    CHECK(structData.chunks.size() == 1);
    CHECK(structData.buffers.size() == 0);

    REQUIRE(structData.chunks[0]);

    const SDChunk &chunk = *structData.chunks[0];

    CHECK(chunk.data.children.size() == 5);

    for(SDObject *o : chunk.data.children)
      REQUIRE(o);

    int childIdx = 0;

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "enumVal");
      CHECK(o.type.basetype == SDBasic::Enum);
      CHECK(o.type.flags == SDTypeFlags::HasCustomString);

      CHECK(o.data.basic.u == AnotherEnumValue);
      CHECK(o.data.str == "AnotherEnumValue");
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "sparseStructArray");
      CHECK(o.type.basetype == SDBasic::Array);
      CHECK(o.type.byteSize == 10);
      CHECK(o.type.flags == SDTypeFlags::NoFlags);
      CHECK(o.data.children.size() == 10);

      for(SDObject *child : o.data.children)
      {
        CHECK(child->type.basetype == SDBasic::Struct);
        CHECK(child->type.name == "struct1");
        CHECK(child->type.byteSize == sizeof(struct1));
        CHECK(child->data.children.size() == 4);
        CHECK(child->data.children[0]->type.basetype == SDBasic::Float);
        CHECK(child->data.children[0]->type.byteSize == 4);
        CHECK(child->data.children[0]->name == "x");
        CHECK(child->data.children[1]->type.basetype == SDBasic::Float);
        CHECK(child->data.children[1]->type.byteSize == 4);
        CHECK(child->data.children[1]->name == "y");
        CHECK(child->data.children[2]->type.basetype == SDBasic::Float);
        CHECK(child->data.children[2]->type.byteSize == 4);
        CHECK(child->data.children[2]->name == "width");
        CHECK(child->data.children[3]->type.basetype == SDBasic::Float);
        CHECK(child->data.children[3]->type.byteSize == 4);
        CHECK(child->data.children[3]->name == "height");
      }

      CHECK(o.data.children[0]->data.children[0]->data.basic.d == 0.0f);
      CHECK(o.data.children[0]->data.children[1]->data.basic.d == 0.0f);
      CHECK(o.data.children[0]->data.children[2]->data.basic.d == 0.0f);
      CHECK(o.data.children[0]->data.children[3]->data.basic.d == 0.0f);

      CHECK(o.data.children[5]->data.children[0]->data.basic.d == 1.0f);
      CHECK(o.data.children[5]->data.children[1]->data.basic.d == 2.0f);
      CHECK(o.data.children[5]->data.children[2]->data.basic.d == 3.0f);
      CHECK(o.data.children[5]->data.children[3]->data.basic.d == 4.0f);

      CHECK(o.data.children[8]->data.children[0]->data.basic.d == 10.0f);
      CHECK(o.data.children[8]->data.children[1]->data.basic.d == 20.0f);
      CHECK(o.data.children[8]->data.children[2]->data.basic.d == 30.0f);
      CHECK(o.data.children[8]->data.children[3]->data.basic.d == 40.0f);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "complex");
      CHECK(o.type.name == "struct2");
      CHECK(o.type.basetype == SDBasic::Struct);
      CHECK(o.type.byteSize == sizeof(struct2));
      CHECK(o.type.flags == SDTypeFlags::NoFlags);
      CHECK(o.data.children.size() == 3);

      {
        SDObject &c = *o.data.children[0];

        CHECK(c.name == "name");
        CHECK(c.type.name == "string");
        CHECK(c.type.basetype == SDBasic::String);
        CHECK(c.type.flags == SDTypeFlags::NoFlags);

        CHECK(c.data.str == "A complex object");
      }

      {
        SDObject &c = *o.data.children[1];

        CHECK(c.name == "floats");
        CHECK(c.type.basetype == SDBasic::Array);
        CHECK(c.type.flags == SDTypeFlags::NoFlags);
        CHECK(c.data.children.size() == 3);
        for(SDObject *ch : c.data.children)
        {
          CHECK(ch->type.basetype == SDBasic::Float);
          CHECK(ch->type.byteSize == 4);
        }

        CHECK(c.data.children[0]->data.basic.d == 1.2f);
        CHECK(c.data.children[1]->data.basic.d == 3.4f);
        CHECK(c.data.children[2]->data.basic.d == 5.6f);
      }

      {
        SDObject &c = *o.data.children[2];

        CHECK(c.name == "viewports");
        CHECK(c.type.basetype == SDBasic::Array);
        CHECK(c.type.flags == SDTypeFlags::NoFlags);
        CHECK(c.data.children.size() == 4);
        for(SDObject *ch : c.data.children)
        {
          CHECK(ch->type.basetype == SDBasic::Struct);
          CHECK(ch->type.name == "struct1");
        }

        CHECK(c.data.children[0]->data.children[0]->data.basic.d == 512.0f);
        CHECK(c.data.children[0]->data.children[1]->data.basic.d == 0.0f);
        CHECK(c.data.children[0]->data.children[2]->data.basic.d == 256.0f);
        CHECK(c.data.children[0]->data.children[3]->data.basic.d == 256.0f);
      }
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "inputParam1");
      CHECK(o.type.basetype == SDBasic::Struct);
      CHECK(o.type.flags == SDTypeFlags::Nullable);

      CHECK(o.data.children[0]->data.basic.d == 9.0f);
      CHECK(o.data.children[1]->data.basic.d == 9.9f);
      CHECK(o.data.children[2]->data.basic.d == 9.99f);
      CHECK(o.data.children[3]->data.basic.d == 9.999f);
    }

    {
      SDObject &o = *chunk.data.children[childIdx++];

      CHECK(o.name == "inputParam2");
      CHECK(o.type.basetype == SDBasic::Null);
      CHECK(o.type.flags == SDTypeFlags::Nullable);
    }

    StreamWriter *rewriteBuf = new StreamWriter(StreamWriter::DefaultScratchSize);

    {
      WriteSerialiser rewrite(rewriteBuf, Ownership::Nothing);

      rewrite.WriteStructuredFile(structData, NULL);
    }

    // must be bitwise identical to the original serialised data.
    REQUIRE(rewriteBuf->GetOffset() == buf->GetOffset());
    CHECK_FALSE(memcmp(rewriteBuf->GetData(), buf->GetData(), (size_t)rewriteBuf->GetOffset()));

    delete rewriteBuf;
  }

  delete buf;
};

#endif    // ENABLED(ENABLE_UNIT_TESTS)
