
#ifndef REALM_SYNC_CHANGESET_ENCODER_HPP
#define REALM_SYNC_CHANGESET_ENCODER_HPP

#include <realm/sync/changeset.hpp>

namespace realm {
namespace sync {

struct ChangesetEncoder {
    using Buffer = util::AppendBuffer<char>;

    Buffer release() noexcept;
    void reset() noexcept;
    Buffer& buffer() noexcept;
    InternString intern_string(StringData);

    void set_intern_string(uint32_t index, StringBufferRange);
    // FIXME: This doesn't copy the input, but the drawback is that there can
    // only be a single StringBufferRange per instruction. Luckily, no
    // instructions exist that require two or more.
    StringBufferRange add_string_range(StringData);
    void operator()(const Instruction&);

#define REALM_DEFINE_INSTRUCTION_HANDLER(X) void operator()(const Instruction::X&);
    REALM_FOR_EACH_INSTRUCTION_TYPE(REALM_DEFINE_INSTRUCTION_HANDLER)
#undef REALM_DEFINE_INSTRUCTION_HANDLER

    void encode_single(const Changeset& log);

protected:
    template <class E>
    static void encode(E& encoder, const Instruction&);

    StringData get_string(StringBufferRange) const noexcept;

private:
    template <class... Args>
    void append(Instruction::Type t, Args&&...);
    template <class... Args>
    void append_path_instr(Instruction::Type t, const Instruction::PathInstruction&, Args&&...);
    void append_string(StringBufferRange); // does not intern the string
    void append_bytes(const void*, size_t);

    template <class T>
    void append_int(T);
    void append_value(const Instruction::PrimaryKey&);
    void append_value(const Instruction::Payload&);
    void append_value(const Instruction::Payload::Link&);
    void append_value(Instruction::Payload::Type);
    void append_value(util::Optional<Instruction::Payload::Type>);
    void append_value(Instruction::CollectionType);
    void append_value(const Instruction::Path&);
    void append_value(DataType);
    void append_value(bool);
    void append_value(uint8_t);
    void append_value(int64_t);
    void append_value(uint32_t);
    void append_value(uint64_t);
    void append_value(float);
    void append_value(double);
    void append_value(InternString);
    void append_value(GlobalKey);
    void append_value(Timestamp);
    void append_value(ObjectId);
    void append_value(Decimal128);
    void append_value(UUID);

    Buffer m_buffer;
    std::map<std::string, uint32_t, std::less<>> m_intern_strings_rev;
    std::string_view m_string_range;
};

// Implementation

inline auto ChangesetEncoder::buffer() noexcept -> Buffer&
{
    return m_buffer;
}

inline void ChangesetEncoder::operator()(const Instruction& instr)
{
    encode(*this, instr); // Throws
}

template <class E>
inline void ChangesetEncoder::encode(E& encoder, const Instruction& instr)
{
    instr.visit(encoder); // Throws
}

inline StringData ChangesetEncoder::get_string(StringBufferRange range) const noexcept
{
    const char* data = m_string_range.data() + range.offset;
    std::size_t size = std::size_t(range.size);
    return StringData{data, size};
}

inline void encode_changeset(const Changeset& changeset, ChangesetEncoder::Buffer& out_buffer)
{
    ChangesetEncoder encoder;
    swap(encoder.buffer(), out_buffer);
    encoder.encode_single(changeset); // Throws
    swap(encoder.buffer(), out_buffer);
}

} // namespace sync
} // namespace realm

#endif // REALM_SYNC_CHANGESET_ENCODER_HPP
