// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2016 The Bitcoin Core developers
// Copyright (c) 2017-2025 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#pragma once

#include <serialize.h>

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <ios>
#include <limits>
#include <span>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

template <typename Stream> class OverrideStream {
    Stream *stream;

    const int nType;
    const int nVersion;

public:
    OverrideStream(Stream *stream_, int nType_, int nVersion_)
        : stream(stream_), nType(nType_), nVersion(nVersion_) {}

    template <typename T> OverrideStream<Stream> &operator<<(const T &obj) {
        // Serialize to this stream
        ::Serialize(*this, obj);
        return (*this);
    }

    template <typename T> OverrideStream<Stream> &operator>>(T &&obj) {
        // Unserialize from this stream
        ::Unserialize(*this, obj);
        return (*this);
    }

    void write(const char *pch, size_t nSize) { stream->write(pch, nSize); }

    void read(char *pch, size_t nSize) { stream->read(pch, nSize); }

    int GetVersion() const { return nVersion; }
    int GetType() const { return nType; }
    void ignore(size_t size) { return stream->ignore(size); }
};

template <typename S> OverrideStream<S> WithOrVersion(S *s, int nVersionFlag) {
    return OverrideStream<S>(s, s->GetType(), s->GetVersion() | nVersionFlag);
}

/**
 * Minimal stream for overwriting and/or appending to an existing byte vector.
 *
 * The referenced vector will grow as necessary.
 */
template <typename VecT>
class GenericVectorWriter {
    const int nType;
    const int nVersion;
    VecT &vchData;
    size_t nPos;

    // ensure this is a vector of character-like objects
    static_assert(std::is_standard_layout_v<typename VecT::value_type> && std::is_trivial_v<typename VecT::value_type>
                  && sizeof(typename VecT::value_type) == 1);

public:
    /**
     * @param[in]  nTypeIn Serialization Type
     * @param[in]  nVersionIn Serialization Version (including any flags)
     * @param[in]  vchDataIn  Referenced byte vector to overwrite/append
     * @param[in]  nPosIn Starting position. Vector index where writes should
     * start. The vector will initially grow as necessary to  max(nPosIn,
     * vec.size()). So to append, use vec.size().
     */
    GenericVectorWriter(int nTypeIn, int nVersionIn, VecT &vchDataIn, size_t nPosIn)
        : nType(nTypeIn), nVersion(nVersionIn), vchData(vchDataIn), nPos(nPosIn) {
        if (nPos > vchData.size()) {
            vchData.resize(nPos);
        }
    }
    /**
     * (other params same as above)
     * @param[in]  args  A list of items to serialize starting at nPosIn.
     */
    template <typename... Args>
    GenericVectorWriter(int nTypeIn, int nVersionIn, VecT &vchDataIn, size_t nPosIn, Args &&... args)
        : GenericVectorWriter(nTypeIn, nVersionIn, vchDataIn, nPosIn) {
        ::SerializeMany(*this, std::forward<Args>(args)...);
    }
    void write(const char *pch, size_t nSize) {
        assert(nPos <= vchData.size());
        size_t nOverwrite = std::min(nSize, vchData.size() - nPos);
        if (nOverwrite) {
            std::memcpy(vchData.data() + nPos, pch, nOverwrite);
        }
        if (nOverwrite < nSize) {
            vchData.insert(vchData.end(),
                           reinterpret_cast<const uint8_t *>(pch) + nOverwrite,
                           reinterpret_cast<const uint8_t *>(pch) + nSize);
        }
        nPos += nSize;
    }
    template <typename T> GenericVectorWriter &operator<<(const T &obj) {
        // Serialize to this stream
        ::Serialize(*this, obj);
        return *this;
    }
    int GetVersion() const { return nVersion; }
    int GetType() const { return nType; }
    size_t size() const { return vchData.size(); }
    void seek(size_t nSize) {
        nPos += nSize;
        if (nPos > vchData.size()) {
            vchData.resize(nPos);
        }
    }
};

using CVectorWriter = GenericVectorWriter<std::vector<uint8_t>>; //! for compat. with existing code

/**
 * Minimal stream for reading from an existing vector-like object by reference
 */
template <typename VecT>
class GenericVectorReader {
    int m_type;
    int m_version;
    const VecT &m_data;
    size_t m_pos = 0;

    // ensure this is a vector of character-like objects
    static_assert(std::is_standard_layout_v<typename VecT::value_type> && std::is_trivial_v<typename VecT::value_type>
                  && sizeof(typename VecT::value_type) == 1);

public:
    /**
     * @param[in]  type Serialization Type
     * @param[in]  version Serialization Version (including any flags)
     * @param[in]  data Referenced byte vector to overwrite/append
     * @param[in]  pos Starting position. Vector index where reads should start.
     */
    GenericVectorReader(int type, int version, const VecT &data, size_t pos)
        : m_type(type), m_version(version), m_data(data), m_pos(pos) {
        if (m_pos > m_data.size()) {
            throw std::ios_base::failure(
                "VectorReader(...): end of data (m_pos > m_data.size())");
        }
    }

    /**
     * (other params same as above)
     * @param[in]  args  A list of items to deserialize starting at pos.
     */
    template <typename... Args>
    GenericVectorReader(int type, int version, const VecT &data, size_t pos, Args &&... args)
        : GenericVectorReader(type, version, data, pos) {
        ::UnserializeMany(*this, std::forward<Args>(args)...);
    }

    template <typename T> GenericVectorReader &operator>>(T &&obj) {
        // Unserialize from this stream
        ::Unserialize(*this, obj);
        return *this;
    }

    int GetVersion() const { return m_version; }
    void SetVersion(int version) { m_version = version; }

    int GetType() const { return m_type; }
    void SetType(int type) { m_type = type; }

    size_t GetPos() const { return m_pos; }

    size_t size() const { return m_data.size() - m_pos; }
    bool empty() const { return m_data.size() == m_pos; }

    void read(char *dst, size_t n) {
        if (n == 0) {
            return;
        }

        // Read from the beginning of the buffer
        size_t pos_next = m_pos + n;
        if (pos_next > m_data.size()) {
            throw std::ios_base::failure("VectorReader::read(): end of data");
        }
        std::memcpy(dst, m_data.data() + m_pos, n);
        m_pos = pos_next;
    }

    void ignore(size_t n) {
        // Ignore bytes in the read buffer
        if (n == 0) return;
        size_t pos_next = m_pos + n;
        if (pos_next > m_data.size()) throw std::ios_base::failure("VectorReader::ignore(): end of data");
        m_pos = pos_next;
    }

};

using VectorReader = GenericVectorReader<std::vector<uint8_t>>; //! for compat. with existing code

/**
 * Double ended buffer combining vector and stream-like interfaces.
 *
 * >> and << read and write unformatted data using the above serialization
 * templates. Fills with data in linear time; some stringstream implementations
 * take N^2 time.
 */
class CDataStream {
protected:
    std::vector<char> vch;
    unsigned int nReadPos;

    int nType;
    int nVersion;

public:
    using vector_type = decltype(vch);
    using allocator_type = vector_type::allocator_type;
    using size_type = vector_type::size_type;
    using difference_type = vector_type::difference_type;
    using reference = vector_type::reference;
    using const_reference = vector_type::const_reference;
    using value_type = vector_type::value_type;
    using iterator = vector_type::iterator;
    using const_iterator = vector_type::const_iterator;
    using reverse_iterator = vector_type::reverse_iterator;

    explicit CDataStream(int nTypeIn, int nVersionIn) {
        Init(nTypeIn, nVersionIn);
    }

    CDataStream(const_iterator pbegin, const_iterator pend, int nTypeIn,
                int nVersionIn)
        : vch(pbegin, pend) {
        Init(nTypeIn, nVersionIn);
    }

    CDataStream(const char *pbegin, const char *pend, int nTypeIn,
                int nVersionIn)
        : vch(pbegin, pend) {
        Init(nTypeIn, nVersionIn);
    }

    CDataStream(const std::vector<char> &vchIn, int nTypeIn, int nVersionIn)
        : vch(vchIn.begin(), vchIn.end()) {
        Init(nTypeIn, nVersionIn);
    }

    CDataStream(const std::vector<uint8_t> &vchIn, int nTypeIn, int nVersionIn)
        : vch(vchIn.begin(), vchIn.end()) {
        Init(nTypeIn, nVersionIn);
    }

    template <typename... Args>
    CDataStream(int nTypeIn, int nVersionIn, Args &&... args) {
        Init(nTypeIn, nVersionIn);
        ::SerializeMany(*this, std::forward<Args>(args)...);
    }

    void Init(int nTypeIn, int nVersionIn) {
        nReadPos = 0;
        nType = nTypeIn;
        nVersion = nVersionIn;
    }

    CDataStream &operator+=(const CDataStream &b) {
        vch.insert(vch.end(), b.begin(), b.end());
        return *this;
    }

    friend CDataStream operator+(const CDataStream &a, const CDataStream &b) {
        CDataStream ret = a;
        ret += b;
        return (ret);
    }

    std::string str() const { return (std::string(begin(), end())); }

    //
    // Vector subset
    //
    const_iterator begin() const { return vch.begin() + nReadPos; }
    iterator begin() { return vch.begin() + nReadPos; }
    const_iterator end() const { return vch.end(); }
    iterator end() { return vch.end(); }
    size_type size() const { return vch.size() - nReadPos; }
    bool empty() const { return vch.size() == nReadPos; }
    void resize(size_type n, value_type c = 0) { vch.resize(n + nReadPos, c); }
    void reserve(size_type n) { vch.reserve(n + nReadPos); }
    const_reference operator[](size_type pos) const {
        return vch[pos + nReadPos];
    }
    reference operator[](size_type pos) { return vch[pos + nReadPos]; }
    void clear() {
        vch.clear();
        nReadPos = 0;
    }
    iterator insert(iterator it, const char x = char()) {
        return vch.insert(it, x);
    }
    void insert(iterator it, size_type n, const char x) {
        vch.insert(it, n, x);
    }
    value_type *data() { return vch.data() + nReadPos; }
    const value_type *data() const { return vch.data() + nReadPos; }

    void insert(iterator it, std::vector<char>::const_iterator first,
                std::vector<char>::const_iterator last) {
        if (last == first) {
            return;
        }

        assert(last - first > 0);
        if (it == vch.begin() + nReadPos &&
            (unsigned int)(last - first) <= nReadPos) {
            // special case for inserting at the front when there's room
            nReadPos -= (last - first);
            memcpy(&vch[nReadPos], &first[0], last - first);
        } else {
            vch.insert(it, first, last);
        }
    }

    void insert(iterator it, const char *first, const char *last) {
        if (last == first) {
            return;
        }

        assert(last - first > 0);
        if (it == vch.begin() + nReadPos &&
            (unsigned int)(last - first) <= nReadPos) {
            // special case for inserting at the front when there's room
            nReadPos -= (last - first);
            memcpy(&vch[nReadPos], &first[0], last - first);
        } else {
            vch.insert(it, first, last);
        }
    }

    iterator erase(iterator it) {
        if (it == vch.begin() + nReadPos) {
            // special case for erasing from the front
            if (++nReadPos >= vch.size()) {
                // whenever we reach the end, we take the opportunity to clear
                // the buffer
                nReadPos = 0;
                return vch.erase(vch.begin(), vch.end());
            }
            return vch.begin() + nReadPos;
        } else {
            return vch.erase(it);
        }
    }

    iterator erase(iterator first, iterator last) {
        if (first == vch.begin() + nReadPos) {
            // special case for erasing from the front
            if (last == vch.end()) {
                nReadPos = 0;
                return vch.erase(vch.begin(), vch.end());
            } else {
                nReadPos = (last - vch.begin());
                return last;
            }
        } else
            return vch.erase(first, last);
    }

    inline void Compact() {
        vch.erase(vch.begin(), vch.begin() + nReadPos);
        nReadPos = 0;
    }

    bool Rewind(size_type n) {
        // Rewind by n characters if the buffer hasn't been compacted yet
        if (n > nReadPos) {
            return false;
        }
        nReadPos -= n;
        return true;
    }

    //
    // Stream subset
    //
    bool eof() const { return size() == 0; }
    CDataStream *rdbuf() { return this; }
    int in_avail() const { return size(); }

    void SetType(int n) { nType = n; }
    int GetType() const { return nType; }
    void SetVersion(int n) { nVersion = n; }
    int GetVersion() const { return nVersion; }

    void read(char *pch, size_t nSize) {
        if (nSize == 0) {
            return;
        }

        // Read from the beginning of the buffer
        unsigned int nReadPosNext = nReadPos + nSize;
        if (nReadPosNext > vch.size()) {
            throw std::ios_base::failure("CDataStream::read(): end of data");
        }
        memcpy(pch, &vch[nReadPos], nSize);
        if (nReadPosNext == vch.size()) {
            nReadPos = 0;
            vch.clear();
            return;
        }
        nReadPos = nReadPosNext;
    }

    void ignore(int nSize) {
        // Ignore from the beginning of the buffer
        if (nSize < 0) {
            throw std::ios_base::failure(
                "CDataStream::ignore(): nSize negative");
        }
        unsigned int nReadPosNext = nReadPos + nSize;
        if (nReadPosNext >= vch.size()) {
            if (nReadPosNext > vch.size()) {
                throw std::ios_base::failure(
                    "CDataStream::ignore(): end of data");
            }
            nReadPos = 0;
            vch.clear();
            return;
        }
        nReadPos = nReadPosNext;
    }

    void write(const char *pch, size_t nSize) {
        // Write to the end of the buffer
        vch.insert(vch.end(), pch, pch + nSize);
    }

    template <typename Stream> void Serialize(Stream &s) const {
        // Special case: stream << stream concatenates like stream += stream
        if (!vch.empty()) {
            s.write((char *)vch.data(), vch.size() * sizeof(value_type));
        }
    }

    template <typename T> CDataStream &operator<<(const T &obj) {
        // Serialize to this stream
        ::Serialize(*this, obj);
        return (*this);
    }

    template <typename T> CDataStream &operator>>(T &&obj) {
        // Unserialize from this stream
        ::Unserialize(*this, obj);
        return (*this);
    }

    void GetAndClear(vector_type &d) {
        d.insert(d.end(), begin(), end());
        clear();
    }

    /**
     * XOR the contents of this stream with a certain key.
     *
     * @param[in] key    The key used to XOR the data in this stream.
     */
    void Xor(const std::vector<uint8_t> &key) {
        if (key.size() == 0) {
            return;
        }

        for (size_type i = 0, j = 0; i != size(); i++) {
            vch[i] ^= key[j++];

            // This potentially acts on very many bytes of data, so it's
            // important that we calculate `j`, i.e. the `key` index in this way
            // instead of doing a %, which would effectively be a division for
            // each byte Xor'd -- much slower than need be.
            if (j == key.size()) j = 0;
        }
    }
};

template <typename IStream> class BitStreamReader {
private:
    IStream &m_istream;

    /// Buffered byte read in from the input stream. A new byte is read into the
    /// buffer when m_offset reaches 8.
    uint8_t m_buffer{0};

    /// Number of high order bits in m_buffer already returned by previous
    /// Read() calls. The next bit to be returned is at this offset from the
    /// most significant bit position.
    int m_offset{8};

public:
    explicit BitStreamReader(IStream &istream) : m_istream(istream) {}

    /**
     * Read the specified number of bits from the stream. The data is returned
     * in the nbits least significant bits of a 64-bit uint.
     */
    uint64_t Read(int nbits) {
        if (nbits < 0 || nbits > 64) {
            throw std::out_of_range("nbits must be between 0 and 64");
        }

        uint64_t data = 0;
        while (nbits > 0) {
            if (m_offset == 8) {
                m_istream >> m_buffer;
                m_offset = 0;
            }

            int bits = std::min(8 - m_offset, nbits);
            data <<= bits;
            data |= static_cast<uint8_t>(m_buffer << m_offset) >> (8 - bits);
            m_offset += bits;
            nbits -= bits;
        }
        return data;
    }
};

template <typename OStream> class BitStreamWriter {
private:
    OStream &m_ostream;

    /// Buffered byte waiting to be written to the output stream. The byte is
    /// written buffer when m_offset reaches 8 or Flush() is called.
    uint8_t m_buffer{0};

    /// Number of high order bits in m_buffer already written by previous
    /// Write() calls and not yet flushed to the stream. The next bit to be
    /// written to is at this offset from the most significant bit position.
    int m_offset{0};

public:
    explicit BitStreamWriter(OStream &ostream) : m_ostream(ostream) {}

    ~BitStreamWriter() { Flush(); }

    /**
     * Write the nbits least significant bits of a 64-bit int to the output
     * stream. Data is buffered until it completes an octet.
     */
    void Write(uint64_t data, int nbits) {
        if (nbits < 0 || nbits > 64) {
            throw std::out_of_range("nbits must be between 0 and 64");
        }

        while (nbits > 0) {
            int bits = std::min(8 - m_offset, nbits);
            m_buffer |= (data << (64 - nbits)) >> (64 - 8 + m_offset);
            m_offset += bits;
            nbits -= bits;

            if (m_offset == 8) {
                Flush();
            }
        }
    }

    /**
     * Flush any unwritten bits to the output stream, padding with 0's to the
     * next byte boundary.
     */
    void Flush() {
        if (m_offset == 0) {
            return;
        }

        m_ostream << m_buffer;
        m_buffer = 0;
        m_offset = 0;
    }
};

/**
 * Non-refcounted RAII wrapper for FILE*
 *
 * Will automatically close the file when it goes out of scope if not null. If
 * you're returning the file pointer, return file.release(). If you need to
 * close the file early, use file.fclose() instead of fclose(file).
 */
class CAutoFile {
    int nType;
    int nVersion;

    FILE *file;

    void setNull();

public:
    CAutoFile(FILE *filenew, int nTypeIn, int nVersionIn);

    // Alow move-construct to transfer ownership
    CAutoFile(CAutoFile &&o);

    // Allow move-assign to transfer ownership
    CAutoFile &operator=(CAutoFile &&o);

    ~CAutoFile();

    // Disallow copies
    CAutoFile(const CAutoFile &) = delete;
    CAutoFile &operator=(const CAutoFile &) = delete;

    void fclose();

    /**
     * Get wrapped FILE* with transfer of ownership.
     * @note This will invalidate the CAutoFile object, and makes it the
     * responsibility of the caller of this function to clean up the returned
     * FILE*.
     */
    FILE *release();

    /**
     * Get wrapped FILE* without transfer of ownership.
     * @note Ownership of the FILE* will remain with this class. Use this only
     * if the scope of the CAutoFile outlives use of the passed pointer.
     */
    FILE *Get() const { return file; }

    /** Return true if the wrapped FILE* is nullptr, false otherwise. */
    bool IsNull() const { return file == nullptr; }

    //
    // Stream subset
    //
    int GetType() const { return nType; }
    int GetVersion() const { return nVersion; }

    void read(std::span<std::byte> buf);
    void read(std::byte *pbuf, size_t nSize) { read(std::span{pbuf, nSize}); }
    void read(char *pch, size_t nSize) { read(std::as_writable_bytes(std::span{pch, nSize})); }
    /** Implementation detail, only used internally. */
    std::size_t detail_fread(std::span<std::byte> dst);

    void ignore(size_t nSize);

    void write(std::span<const std::byte> buf);
    void write(const std::byte *pbuf, size_t nSize) { write(std::span{pbuf, nSize}); }
    void write(const char *pch, size_t nSize) { write(std::as_bytes(std::span{pch, nSize})); }

    template <typename T> CAutoFile &operator<<(const T &obj) {
        // Serialize to this stream
        if (!file) {
            throw std::ios_base::failure("CAutoFile::operator<<: file handle is nullptr");
        }
        ::Serialize(*this, obj);
        return *this;
    }

    template <typename T> CAutoFile &operator>>(T &&obj) {
        // Unserialize from this stream
        if (!file) {
            throw std::ios_base::failure("CAutoFile::operator>>: file handle is nullptr");
        }
        ::Unserialize(*this, obj);
        return *this;
    }
};

using DataBuffer = std::vector<std::byte>;

/**
 * Non-refcounted RAII wrapper around a FILE* that implements a ring buffer to
 * deserialize from. It guarantees the ability to rewind a given number of
 * bytes.
 *
 * Will automatically close the file when it goes out of scope if not null. If
 * you need to close the file early, use file.fclose() instead of fclose(file).
 */
class CBufferedFile {
private:
    const int nType;
    const int nVersion;

    //! source file
    FILE *src;
    //! how many bytes have been read from source
    uint64_t nSrcPos;
    //! how many bytes have been read from this
    uint64_t nReadPos;
    //! up to which position we're allowed to read
    uint64_t nReadLimit;
    //! how many bytes we guarantee to rewind
    uint64_t nRewind;
    //! the buffer
    DataBuffer byteBuf;

protected:
    //! read data from the source to fill the buffer
    bool Fill();

public:
    CBufferedFile(FILE *fileIn, uint64_t nBufSize, uint64_t nRewindIn, int nTypeIn, int nVersionIn);
    ~CBufferedFile();

    // Disallow copies
    CBufferedFile(const CBufferedFile &) = delete;
    CBufferedFile &operator=(const CBufferedFile &) = delete;

    int GetVersion() const { return nVersion; }
    int GetType() const { return nType; }

    void fclose();

    //! check whether we're at the end of the source file
    bool eof() const { return nReadPos == nSrcPos && std::feof(src); }

    //! read a number of bytes
    void read(std::span<std::byte> outBuf);
    void read(std::byte *pbuf, size_t nSize) { read(std::span{pbuf, nSize}); }
    void read(char *pch, size_t nSize) { read(std::as_writable_bytes(std::span{pch, nSize})); }

    //! return the current reading position
    uint64_t GetPos() const { return nReadPos; }

    //! rewind to a given reading position
    bool SetPos(uint64_t nPos);

    bool Seek(uint64_t nPos);

    //! Prevent reading beyond a certain position. No argument removes the
    //! limit.
    bool SetLimit(uint64_t nPos = std::numeric_limits<uint64_t>::max());

    template <typename T> CBufferedFile &operator>>(T &&obj) {
        // Unserialize from this stream
        ::Unserialize(*this, obj);
        return *this;
    }

    //! search for a given byte in the stream, and remain positioned on it
    void FindByte(std::byte ch);
    void FindByte(char ch) { FindByte(static_cast<std::byte>(ch)); }
};

namespace detail {
template <typename S>
class BufferedCommon {
protected:
    S stream;
    DataBuffer buf;
    size_t buf_pos;

    //! Requires stream has a lifetime longer than this instance
    BufferedCommon(S&& streamIn, size_t size, size_t pos)
        : stream{std::move(streamIn)}, buf(size), buf_pos{pos} {}

public:
    // Stream API
    int GetType() const { return stream.GetType(); }
    int GetVersion() const { return stream.GetVersion(); }

    // Helper methods
    const S& GetStream() const { return stream; }
};
} // namespace detail

/**
 * Wrapper that buffers reads from an underlying stream.
 * Requires underlying stream to support read() and detail_fread() calls
 * to support fixed-size and variable-sized reads, respectively.
 *
 * Also requires that the underlying stream properly support move semantics.
 */
template <typename S>
class BufferedReader : public detail::BufferedCommon<S> {
public:
    //! Requires stream has a lifetime longer than this instance
    explicit BufferedReader(S streamIn, size_t size = 1 << 16)
        : detail::BufferedCommon<S>(std::move(streamIn), size, size) {}

    void read(std::span<std::byte> dst) {
        if (const size_t available = std::min(dst.size(), this->buf.size() - this->buf_pos)) {
            std::memcpy(dst.data(), this->buf.data() + this->buf_pos, available);
            this->buf_pos += available;
            dst = dst.subspan(available);
        }
        if (dst.size()) {
            assert(this->buf_pos == this->buf.size());
            this->stream.read(dst);

            this->buf_pos = 0;
            this->buf.resize(this->stream.detail_fread(this->buf));
        }
    }

    // Legacy API support
    void read(char *pch, size_t nSize) { read(std::as_writable_bytes(std::span{pch, nSize})); }

    template <typename T>
    BufferedReader& operator>>(T&& obj) {
        ::Unserialize(*this, obj);
        return *this;
    }
};

/**
 * Wrapper that buffers writes to an underlying stream.
 * Requires underlying stream to support write() method.
 *
 * Also requires that the underlying stream properly support move semantics.
 */
template <typename S>
class BufferedWriter : public detail::BufferedCommon<S> {
public:
    //! Requires stream has a lifetime longer than this instance
    explicit BufferedWriter(S streamIn, size_t size = 1 << 16)
        : detail::BufferedCommon<S>(std::move(streamIn), size, 0) {}

    ~BufferedWriter() { flush(); }

    void flush() {
        if (this->buf_pos) {
            this->stream.write(std::span{this->buf}.first(this->buf_pos));
        }
        this->buf_pos = 0;
    }

    void write(std::span<const std::byte> src) {
        while (const size_t available = std::min(src.size(), this->buf.size() - this->buf_pos)) {
            std::memcpy(this->buf.data() + this->buf_pos, src.data(), available);
            this->buf_pos += available;
            if (this->buf_pos == this->buf.size()) flush();
            src = src.subspan(available);
        }
    }

    // Legacy API support
    void write(const char *pch, size_t nSize) { write(std::as_bytes(std::span{pch, nSize})); }

    template <typename T>
    BufferedWriter& operator<<(const T& obj) {
        ::Serialize(*this, obj);
        return *this;
    }
};
