text: #pragma once
  
  #include <khepri/io/container_stream.hpp>
  #include <khepri/io/exceptions.hpp>
  
  #include <array>
  #include <cassert>
  #include <type_traits>
  
  namespace khepri::io {
  namespace {
  
  // Khepri Container File
  constexpr auto MAGIC = std::array<char, 3>{'K', 'C', 'F'};
  
  // File format version
  constexpr auto FORMAT_VERSION = 1;
  
  void require(bool condition)
  {
      if (!condition) {
          throw InvalidFormatError();
      }
  }
  } // namespace
  
  ContainerStream::ContainerStream(Stream& underlying_stream, content_type_id type_id,
                                   open_mode open_mode)
      : m_underlying_stream(&underlying_stream), m_open_mode(open_mode)
  {
      if (m_open_mode == open_mode::read) {
          if (!underlying_stream.readable()) {
              throw ArgumentError();
          }
  
          // Read the file header
          std::remove_const_t<decltype(MAGIC)> file_magic;
          underlying_stream.read(file_magic.data(), file_magic.size());
          require(file_magic == MAGIC);
  
          const auto version = underlying_stream.read_byte();
          require(version == FORMAT_VERSION);
  
          const auto file_type_id = underlying_stream.read_uint();
          require(type_id == file_type_id);
  
          const auto flags = underlying_stream.read_uint();
          require(flags == 0);
  
          m_content_size = underlying_stream.read_uint();
  
          if (underlying_stream.seekable()) {
              m_content_start = underlying_stream.seek(0, seek_origin::current);
          }
      } else {
          // When writing, we must be able to seek because we have to write
          // the file size afterwards.
          if (!underlying_stream.seekable() || !underlying_stream.writable()) {
              throw ArgumentError();
          }
  
          // Write the file header
          underlying_stream.write(MAGIC.data(), MAGIC.size());
          underlying_stream.write_byte(FORMAT_VERSION);
          underlying_stream.write_uint(type_id);
          underlying_stream.write_uint(0); // flags
          underlying_stream.write_uint(0); // size
  
          m_content_start = underlying_stream.seek(0, seek_origin::current);
      }
  }
  
  void ContainerStream::close()
  {
      if (m_underlying_stream != nullptr && m_open_mode == open_mode::write) {
          if (m_content_size <= std::numeric_limits<std::uint32_t>::max()) {
              const auto size_offset =
                  m_content_start - static_cast<long long>(sizeof(std::uint32_t));
              m_underlying_stream->seek(size_offset, seek_origin::begin);
              m_underlying_stream->write_uint(static_cast<std::uint32_t>(m_content_size));
          }
          m_underlying_stream = nullptr;
      }
  }
  
  std::size_t ContainerStream::read(void* buffer, std::size_t count)
  {
      if (!readable()) {
          throw Error("container not opened for reading");
      }
  
      count           = std::min<std::size_t>(count, m_content_size - m_position);
      const auto read = m_underlying_stream->read(buffer, count);
      m_position += static_cast<long long>(read);
      return read;
  }
  
  std::size_t ContainerStream::write(const void* buffer, std::size_t count)
  {
      if (!writable()) {
          throw Error("container not opened for writing");
      }
  
      const auto written = m_underlying_stream->write(buffer, count);
      m_position += static_cast<long long>(written);
      m_content_size = std::max(m_content_size, m_position);
      return written;
  }
  
  long long ContainerStream::seek(long long offset, seek_origin origin)
  {
      if (!seekable()) {
          throw Error("container does not support seeking");
      }
  
      long long position = 0;
      switch (origin) {
      case seek_origin::begin:
          offset   = std::max(0LL, std::min(m_content_size, offset));
          position = m_underlying_stream->seek(m_content_start + offset, seek_origin::begin);
          break;
  
      case seek_origin::current:
          position = m_underlying_stream->seek(0, seek_origin::current) + offset;
          if (m_open_mode == open_mode::read) {
              position = std::min(position, m_content_start + m_content_size);
          }
          position = std::max(position, m_content_start);
          break;
  
      case seek_origin::end:
          position =
              (m_open_mode == open_mode::read)
                  ? m_underlying_stream->seek(m_content_start + m_content_size, seek_origin::begin)
                  : m_underlying_stream->seek(0, seek_origin::end);
          break;
  
      default:
          assert(false);
          break;
      }
  
      assert(position >= m_content_start);
      return position - m_content_start;
  }
  
  } // namespace khepri::io
  