//
// Created by Dan Crosby on 11/18/21.
//

#ifndef EAGLE0_FLATBUFFERWRAPPER_HPP
#define EAGLE0_FLATBUFFERWRAPPER_HPP

#include <flatbuffers/flatbuffers.h>

#include <utility>

#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"

namespace shardok {

class WrapperAllocator {
public:
    virtual auto allocate(size_t size) -> uint8_t* = 0;
    virtual auto deallocate(uint8_t* buf, size_t size) -> void = 0;

    virtual ~WrapperAllocator() = default;

    static auto GetDefaultAllocator() -> WrapperAllocator*;
};

template<class FB>
class Wrapper {
private:
    size_t size;
    size_t offset;
    uint8_t* buffer;

    static auto CopyBuffer(const uint8_t* data, const size_t size) -> uint8_t* {
        auto* b = WrapperAllocator::GetDefaultAllocator()->allocate(size);
        memcpy(b, data, size);
        return b;
    }

    static auto CopyBuffer(const byte_vector& bytes) -> uint8_t* {
        return CopyBuffer(bytes.data(), bytes.size());
    }

    static auto CopyBuffer(const Wrapper& copiedFrom) -> uint8_t* {
        return CopyBuffer(copiedFrom.buffer, copiedFrom.size);
    }

public:
    explicit Wrapper(flatbuffers::FlatBufferBuilder& fbb) : size(0), offset(0) {
        const uint8_t* releasedBuf = fbb.ReleaseRaw(size, offset);

        if (releasedBuf == nullptr || size == 0) {
            buffer = nullptr;
        } else {
            buffer = CopyBuffer(releasedBuf, size);
        }
        delete[] releasedBuf;
    }

    explicit Wrapper(uint8_t* allocatedBytes, const size_t offset, const size_t size)
        : size(size),
          offset(offset),
          buffer(CopyBuffer(allocatedBytes, size)) {}

    Wrapper() : size(0), offset(0), buffer(nullptr) {}
    Wrapper(Wrapper&& movedFrom) noexcept
        : size(movedFrom.size),
          offset(movedFrom.offset),
          buffer(std::exchange(movedFrom.buffer, nullptr)) {}
    Wrapper(const Wrapper& copiedFrom)
        : size(copiedFrom.size),
          offset(copiedFrom.offset),
          buffer(CopyBuffer(copiedFrom)){};

    auto operator=(const Wrapper& other) -> Wrapper& {
        if (&other != this) {
            if (other.size > size) {
                if (buffer != nullptr) {
                    WrapperAllocator::GetDefaultAllocator()->deallocate(buffer, size);
                }
                buffer = CopyBuffer(other.buffer, other.size);
            } else {
                memcpy(buffer, other.buffer, other.size);
            }
            memcpy(buffer, other.buffer, other.size);

            size = other.size;
            offset = other.offset;
        }

        return *this;
    }

    auto operator=(Wrapper&& other) noexcept -> Wrapper& {
        std::swap(size, other.size);
        std::swap(offset, other.offset);
        std::swap(buffer, other.buffer);
        return *this;
    }

    ~Wrapper() {
        if (buffer != nullptr) {
            WrapperAllocator::GetDefaultAllocator()->deallocate(buffer, size);
        }
    }

    operator FB*() { return Get(); }              // NOLINT(*-explicit-constructor)
    operator const FB*() const { return Get(); }  // NOLINT(*-explicit-constructor)

    auto Get() const -> const FB* { return flatbuffers::GetRoot<FB>(buffer + offset); }
    auto Get() -> FB* { return flatbuffers::GetMutableRoot<FB>(buffer + offset); }
    auto operator->() const -> const FB* { return flatbuffers::GetRoot<FB>(buffer + offset); }
    auto operator->() -> FB* { return flatbuffers::GetMutableRoot<FB>(buffer + offset); }

    [[nodiscard]] auto SaveTo(const std::string& path) const -> bool {
        return FilesystemUtils::AtomicallySaveToPath(path, ToByteVector());
    }
    static auto LoadFrom(const std::string& path) -> Wrapper {
        return FromByteVector(FilesystemUtils::LoadFromPath(path));
    }

    [[nodiscard]] auto ToByteString() const -> string {
        char* str = (char*)malloc(sizeof(size_t) + size);
        memcpy(str, &offset, sizeof(size_t));
        memcpy(str + sizeof(size_t), reinterpret_cast<char const*>(buffer), size);
        return string(str, sizeof(size_t) + size);
    }
    static auto FromByteString(const string& str) -> Wrapper {
        size_t offset;
        memcpy(&offset, str.data(), sizeof(size_t));

        size_t size = str.size() - sizeof(size_t);
        auto toReturn = Wrapper((uint8_t*)str.data() + sizeof(size_t), offset, size);

        return toReturn;
    }

    [[nodiscard]] auto ToByteVector() const -> byte_vector { return byte_vector(ToByteString()); }
    static auto FromByteVector(const byte_vector& bv) -> Wrapper {
        size_t offset;
        memcpy(&offset, bv.data(), sizeof(size_t));

        size_t size = bv.size() - sizeof(size_t);
        auto toReturn = Wrapper(const_cast<uint8_t*>(bv.data()) + sizeof(size_t), offset, size);

        return toReturn;
    }
};

}  // namespace shardok

#endif  // EAGLE0_FLATBUFFERWRAPPER_HPP
