#pragma once

#include "data.hpp"
#include "../data/mem.hpp"

namespace net {

//TODO: preallocate static data pool

/// Helper to write allocated packets
class PacketWriter final {
public:
    PacketWriter(size_t size): buffer((uint8_t*)malloc(size), size), visible_size(size) { }
    PacketWriter(uint8_t type, size_t data_size): PacketWriter(sizeof(type) + data_size) {
        write(type);
    }
    PacketWriter(net::client_packet_type type, size_t data_size): PacketWriter((uint8_t)type, data_size) { }
    PacketWriter(net::server_packet_type type, size_t data_size): PacketWriter((uint8_t)type, data_size) { }
    ~PacketWriter() {
        if (buffer.ptr != nullptr)
            free(buffer.ptr);
    }

    void write(const void* data, size_t len) {
        buffer.write((uint8_t*)data, len);
    }
    template<typename D>
    void write(const D& d) {
        write(&d, sizeof(d));
    }

    constexpr size_t getCursor() const { return buffer.cur; }
    void writeAt(size_t pos, const void* data, size_t len) {
        assert(pos + len <= buffer.siz);
        memcpy(buffer.ptr + pos, data, len);
    }
    template<typename D>
    void writeAt(size_t pos, const D& d) {
        writeAt(pos, &d, sizeof(d));
    }

    void reserve(size_t target) {
        if (target >= buffer.siz - buffer.cur) {
            const auto size = target + buffer.cur;
            buffer.ptr = (uint8_t *)realloc(buffer.ptr, size);
            buffer.siz = size;
        }
    }
    void resize(size_t target) {
        reserve(target);
        visible_size = target;
    }

    void writePush(const void* data, size_t len) {
        if (buffer.cur + len > buffer.siz)
            resize(buffer.cur + len);
        write(data, len);
    }
    template<typename D>
    void writePush(const D& d) {
        writePush(&d, sizeof(d));
    }

    struct varying_part {
        varying_part(data::in_view &buffer, size_t& p_size): buffer(buffer), parent_size(p_size), visible_size(0) { }
        ~varying_part() {
            buffer.cur += visible_size;
            parent_size += visible_size;
        }

        data::in_view &buffer;
        size_t &parent_size;
        size_t visible_size;

        constexpr size_t size() const { return visible_size; }
        void* data() { return buffer.writeTo(0); }
        void reserve(size_t target) {
            if (target >= buffer.siz - buffer.cur) {
                const auto size = target + buffer.cur;
                buffer.ptr = (uint8_t *)realloc(buffer.ptr, size);
                buffer.siz = size;
            }
        }
        void resize(size_t target) {
            reserve(target);
            visible_size = target;
        }
    };
    /// Only from resize, write, resize down
    varying_part varying() {
        return varying_part(buffer, visible_size);
    }

    bool isFull() const {
        return buffer.cur >= visible_size;
    }

    [[nodiscard]]
    data::out_buffer finish() {
        assert(isFull());
        data::out_buffer cpy(buffer.ptr, visible_size);
        buffer.ptr = nullptr;
        buffer.siz = 0;
        return cpy;
    }

    [[nodiscard]]
    static data::out_buffer Of(net::client_packet_type type, const void* data, size_t size) {
        auto packet = PacketWriter(type, size);
        packet.write(data, size);
        return packet.finish();
    }
    [[nodiscard]]
    static data::out_buffer Of(net::server_packet_type type, const void* data, size_t size) {
        auto packet = PacketWriter(type, size);
        packet.write(data, size);
        return packet.finish();
    }
    template<typename D>
    [[nodiscard]]
    static data::out_buffer Of(net::client_packet_type type, const D& data) {
        return Of(type, &data, sizeof(data));
    }
    template<typename D>
    [[nodiscard]]
    static data::out_buffer Of(net::server_packet_type type, const D& data) {
        return Of(type, &data, sizeof(data));
    }

private:
    data::in_view buffer;
    size_t visible_size;
};

/// Helper to read out_view
class PacketReader final {
public:
    PacketReader(const data::out_view& buf): buffer(buf) { }

    template<typename D>
    const D* read() {
        return buffer.cur + sizeof(D) <= buffer.size() ?
            (const D*)buffer.readFrom(sizeof(D)) : nullptr;
    }
    template<typename D>
    bool read(D& out) {
        const auto ptr = read<D>();
        if (ptr == nullptr)
            return false;

        out = *ptr;
        return true;
    }

    bool skip(size_t size) {
        buffer.readFrom(size);
        return !isFull();
    }

    data::out_view readPart(size_t size) {
        return data::out_view(buffer.readFrom(size), size);
    }
    data::out_view readAll() {
        return readPart(buffer.size() - buffer.cur);
    }

    bool isFull() const {
        return buffer.isDone();
    }

private:
    data::out_view buffer;
};

}