#include "Universe.hpp"

#include <Tracy.hpp>  //NOLINT
#include <common/TracySystem.hpp> // NOLINT
#include <filesystem>
#include <random>

#include "Chunk.hpp"
#include "../../core/world/raycast.hpp"
#include "../../core/world/iterators.hpp"
#include "../../core/world/actions.hpp"
#include "../../core/world/models.hpp"
#include "../../core/net/io.hpp"
#include <glm/common.hpp>

using namespace world::server;

constexpr auto AREAS_FILE = "/areas.idx";
constexpr auto COMPRESSION_PREFIX = (uint8_t)net::server_packet_type::COMPRESSION;
constexpr auto PREDICTABLE = true;

Universe::Universe(const Universe::options &options): host(options.connection,
    [&](net::server::Peer* peer) { return onConnect(peer); },
    [&](net::server::Peer* peer, bool is_app, uint16_t reason) { return onDisconnect(peer, is_app, reason); },
    [&](net::server::Peer* peer, const data::out_view &buf, net::PacketFlags flags) { return onPacket(peer, buf, flags); }
    ), dict_content({options.folderPath + "/zstd.dict", "content/zstd.dict"}, data::out_view(&COMPRESSION_PREFIX, sizeof(COMPRESSION_PREFIX))),
    dicts(dict_content.content()), dict_write_ctx(dicts.make_writer())
{
    setOptions(options);
    folderPath = options.folderPath;
    running = true;

    std::filesystem::create_directories(folderPath);
    {
        std::ifstream index(folderPath + AREAS_FILE);
        if(index.good()) {
            size_t size = 0;
            index.read(reinterpret_cast<char *>(&size), sizeof(size));
            robin_hood::unordered_map<size_t, Area::params> tmp;
            while(!index.eof()) {
                size_t id = UINT32_MAX;
                index.read(reinterpret_cast<char *>(&id), sizeof(size_t));
                Area::params params{voxel_pos(0), 0, generator::params()};
                index.read(reinterpret_cast<char *>(&params.center.x), sizeof(voxel_pos::value_type));
                index.read(reinterpret_cast<char *>(&params.center.y), sizeof(voxel_pos::value_type));
                index.read(reinterpret_cast<char *>(&params.center.z), sizeof(voxel_pos::value_type));
                index.read(reinterpret_cast<char *>(&params.radius), sizeof(int));
                index.read(reinterpret_cast<char *>(&params.properties), sizeof(generator::params));
                [[maybe_unused]]
                auto ok = tmp.emplace(id, params).second;
                assert(ok && "Duplicated area");
                index.peek();
            }
            assert(tmp.size() == size && "Corrupted areas index");
            far_areas = data::generational::vector<Area::params>(tmp);
            LOG_T(far_areas.size() << " areas loaded");
        } else {
            LOG_E("No index file!!! Probably a new world...");
            //TODO: generate universe
            const auto radius = 1 << 4;
            far_areas.emplace(Area::params{glm::multiply(voxel_pos(radius, 0, 0)), radius,
                generator::params(std::in_place_type<generator::RoundPlanet::Params>, radius * CHUNK_LENGTH * 3 / 4, 42)});
            //far_areas.emplace(Area::params{voxel_pos(0), 1 << 20, generator::params(std::in_place_type<generator::Cave::Params>, 42)});
        }
        spawnPoint = voxel_pos(100, 0, 0); //TODO: save in index
        index.close();
    }

    {
        [[maybe_unused]]
        const auto type_id = entities.emplace(world::models::player, glm::usvec3(1), glm::vec3(2));
        assert(type_id == PLAYER_ENTITY_ID);
    }

    // Workers
    for (size_t i = 0; i < std::max<size_t>(1, std::thread::hardware_concurrency() / 2 - 1); i++) {
        workers.emplace_back([&] {
#if TRACY_ENABLE
            tracy::SetThreadName("Chunks");
#endif
            const auto read_ctx = dicts.make_reader();
            const auto write_ctx = dicts.make_writer();
            while (running) {
                if(save_task_t task; saveQueue.pop(task)) {
                    //NOTE: must always save before load to avoid chunk regen
                    //MAYBE: queue.take to avoid concurent write or duplicated work on fast move
                    ZoneScopedN("ProcessSave");
                    std::ostringstream out;
                    if (!task.second.second->isModified()) {
                        out.setstate(std::ios_base::badbit);
                    }
                    const auto average = task.second.second->write(out);
                    const auto rcPos = glm::split(task.second.first);
                    const auto reg = task.first.second->getRegion(folderPath, std::make_pair(task.first.first, rcPos.first));
                    reg->write(rcPos.second, write_ctx, out.str(), std::make_optional(average));
                } else if (std::pair<area_<chunk_pos>, std::shared_ptr<Area>> task; loadQueue.pop(task)) {
                    //MAYBE: loadQueue.take to avoid duplicated work on fast move
                    ZoneScopedN("ProcessLoad");
                    const auto &pos = task.first;
                    const auto rcPos = glm::split(pos.second);
                    const auto reg = task.second->getRegion(folderPath, std::make_pair(pos.first, rcPos.first));
                    std::vector<char> data;
                    if(reg->read(rcPos.second, read_ctx, data)) {
                        ZoneScopedN("ProcessRead");
                        vec_istream idata(data);
                        std::istream iss(&idata);
                        loadedQueue.push({pos, createChunk(iss)});
                    } else {
                        ZoneScopedN("ProcessGenerate");
                        loadedQueue.push({pos, createChunk(pos.second, task.second->getGenerator())});
                    }
                } else {
                    loadQueue.wait();
                }
            }
        });
    }
}
Universe::~Universe() {
    saveAll(false);
    saveAreas();

    running = false;
    loadQueue.notify_all();

    for (auto &worker: workers) {
        if (worker.joinable())
            worker.join();
    }
    LOG_D("Universe disappeared");
}

struct net_client {
    net_client(data::generational::id id): instanceId(id) { }
    data::generational::id instanceId;

    std::vector<std::pair<area_<chunk_pos>, long>> pendingChunks;
    uint32_t chunkEmitRate = 0;
    uint32_t chunkRateEpoch = 0;
    bool popPendingChunk(area_<chunk_pos> &out) {
        if (pendingChunks.empty())
            return false;

        out = pendingChunks.back().first;
        pendingChunks.pop_back();
        return true;
    }
    void pushChunk(const area_<chunk_pos>& in, long dist) {
        for (auto it = pendingChunks.begin(); it != pendingChunks.end(); ++it) {
            if (it->first == in) {
                pendingChunks.erase(it);
                break;
            }
        }

        if (pendingChunks.size() >= net::MAX_PENDING_CHUNK_COUNT) {
            if (dist > pendingChunks.front().second)
                return;
            pendingChunks.erase(pendingChunks.begin());
        }

        auto it = pendingChunks.begin();
        while (it != pendingChunks.end()) {
            if (dist > it->second)
                break;
            ++it;
        }
        pendingChunks.insert(it, std::make_pair(in, dist));
    }

    bool handleEdits = false;
    // MAYBE: client size ordering
    std::queue<world::action::FillShape> pendingEdits;
};

void Universe::saveAll(bool remove) {
    for(auto& area: areas) {
        auto& chunks = area.second->setChunks();
        for (auto it_c = chunks.begin(); it_c != chunks.end(); remove ? it_c = chunks.erase(it_c) : ++it_c) {
            saveQueue.emplace(area, std::make_pair(it_c->first, std::dynamic_pointer_cast<Chunk>(it_c->second)));
        }
    }
    loadQueue.notify_all();

    if (auto size = saveQueue.size(); size > 0) {
        LOG_I("Saving " << size << " chunks");
        const auto SAVE_CHECK_TIME = 500;
        do {
            loadQueue.notify_all();
            std::cout << "\rSaving... " << size << "            " << std::flush;
            std::this_thread::sleep_for(std::chrono::microseconds(SAVE_CHECK_TIME));
            size = saveQueue.size();
        } while (size > 0);
        std::cout << std::endl;
    }
}

// Write areas index (warn: file io)
void Universe::saveAreas() const {
    std::ofstream index(folderPath + AREAS_FILE, std::ios::out | std::ios::binary);
    if(!index.good()) {
        LOG_E("Areas index write error");
        return;
    }
    {
        size_t size = areas.size() + far_areas.size();
        index.write(reinterpret_cast<char *>(&size), sizeof(size));
    }
    std::function write = [&](area_id id, Area::params params) {
        auto idx = id.index;
        index.write(reinterpret_cast<char *>(&idx), sizeof(size_t));
        index.write(reinterpret_cast<char *>(&params.center.x), sizeof(voxel_pos::value_type));
        index.write(reinterpret_cast<char *>(&params.center.y), sizeof(voxel_pos::value_type));
        index.write(reinterpret_cast<char *>(&params.center.z), sizeof(voxel_pos::value_type));
        index.write(reinterpret_cast<char *>(&params.radius), sizeof(int));
        index.write(reinterpret_cast<char *>(&params.properties), sizeof(generator::params)); // MAYBE: use binary structured format (protobuf/msgpack)
    };
    for(const auto& area: areas) {
        write(area.first, area.second->getParams());
    }
    far_areas.iter(write);
    if(!index.good())
        LOG_E("Areas index write error");

    index.close();
}

void Universe::pull() {
    ZoneScopedN("Network");
    host.pull();
    host.iterPeers([&](net::server::Peer *peer) {
        auto data = peer->getCtx<net_client>();
        if (data == nullptr)
            return;

        if (!data->pendingEdits.empty() && peer->queueSize(net::server::queue::EDIT) == 0) {
            peer->send(net::PacketWriter::Of(net::server_packet_type::EDITS, data->pendingEdits.front()));
            data->pendingEdits.pop();
        }

        if (!data->pendingChunks.empty()) {
            //TODO: use congestion
            area_<chunk_pos> pending;
            size_t i = peer->queueSize(net::server::queue::CHUNK);
            while(i <= 4 && data->popPendingChunk(pending)) {
                if (const auto it = areas.find(pending.first); it != areas.end()) {
                    if (const auto chunk = it->second->getChunks().findInRange(pending.second)) {
                        peer->send(serializeChunk(pending, std::dynamic_pointer_cast<Chunk>(chunk.value())), net::server::queue::CHUNK);
                        i++;
                    }
                }
            }

            if (data->pendingChunks.empty())
                peer->send(net::PacketWriter(net::server_packet_type::CHUNK, 0).finish()); //FIXME: must be received after last chunk
        }
    });
}

void Universe::update(float deltaTime) {
    ZoneScopedN("Universe");

    std::vector<voxel_pos> moves;
    {
        moves.reserve(movedPlayers.size());
        for (const auto& id: movedPlayers) {
            if (auto player = findEntity(PLAYER_ENTITY_ID, id))
                moves.push_back(player->pos.as_voxel());
        }
        movedPlayers.clear();
    }

    if (!moves.empty()) {
        ZoneScopedN("Far");
        bool extracted = false;
        far_areas.extract([&](area_id id, Area::params params) {
            for(const auto& move: moves) {
                if(const chunk_pos diff = glm::divide(move - params.center);
                    glm::length2(diff) <= glm::pow2(loadDistance + params.radius)) {

                    LOG_I("Load area " << id.index);
                    areas.emplace(id, std::make_shared<Area>(params));
                    extracted = true;
                    return true;
                }
            }
            return false;
        });
        if(extracted)
            broadcastAreas();
    }
    const auto &players = entities.at(PLAYER_ENTITY_ID).instances;
    { // Update alive areas
        ZoneScopedN("World");
#if TRACY_ENABLE
        size_t chunk_count = 0;
        size_t region_count = 0;
#endif
        const bool queuesEmpty = loadQueue.empty() && saveQueue.empty();
        bool allLazy = true;
        auto it = areas.begin();
        while (it != areas.end()) {
            ZoneScopedN("Area");
            //FIXME: const auto areaChunkChange = it->second->move(glm::vec3(deltaTime));
            const auto areaRange = glm::pow2(keepDistance + it->second->getChunks().getRadius());
            const auto areaDiff = glm::divide(it->second->getOffset().as_voxel());

            std::vector<chunk_pos> inAreaPlayers;
            players.iter([&](const entity_id&, const Entity::Instance& player) {
                const chunk_pos diff = glm::divide(player.pos.as_voxel() - it->second->getOffset().as_voxel());
                if (glm::length2(diff) <= areaRange)
                    inAreaPlayers.push_back(diff);
            });

            auto &chunks = it->second->setChunks();
            if (inAreaPlayers.empty()) {
                auto it_c = chunks.begin();
                while(it_c != chunks.end()) {
                    saveQueue.emplace(*it, std::make_pair(it_c->first, std::dynamic_pointer_cast<Chunk>(it_c->second)));
                    it_c = chunks.erase(it_c);
                }
                loadQueue.notify_all();
                LOG_I("Unload area " << it->first.index);
                [[maybe_unused]]
                auto ok = far_areas.put(it->first, it->second->getParams());
                assert(ok);
                it = areas.erase(it);
                saveAreas();
            } else {
                bool lazyArea = queuesEmpty;
                { // Update alive chunks
                    ZoneScopedN("Alive");
                    auto it_c = chunks.begin();
                    while(it_c != chunks.end()) {
                        if ([&] {
                            const auto keepDist = glm::pow2(keepDistance);
                            for(const auto& diff: inAreaPlayers) {
                                if (glm::length2(diff - it_c->first) < keepDist)
                                    return true;
                            }
                            return false;
                        }()) {
                            updateChunk(it, it_c, areaDiff, deltaTime);
                            ++it_c;
#if TRACY_ENABLE
                            chunk_count++;
#endif
                        } else {
                            saveQueue.emplace(*it, std::make_pair(it_c->first, std::dynamic_pointer_cast<Chunk>(it_c->second))); //MAYBE: take look
                            lazyArea = false;
                            it_c = chunks.erase(it_c);
                        }
                    }
                }
                { // Enqueue missing chunks
                    ZoneScopedN("Missing");
                    auto handle = loadQueue.inserter();
                    for (const auto& to: moves) {
                        const chunk_pos diff = glm::divide(to - it->second->getOffset().as_voxel());
                        if (glm::length2(diff) > areaRange)
                            continue;

                        //TODO: need dist so no easy sphere fill
                        for (int x = -loadDistance; x <= loadDistance; x++) {
                        for (int y = -loadDistance; y <= loadDistance; y++) {
                        for (int z = -loadDistance; z <= loadDistance; z++) {
                            const auto dist2 = x * x + y * y + z * z;
                            if (dist2 <= loadDistance * loadDistance) {
                                const auto p = diff + chunk_pos(x, y, z);
                                if (chunks.inRange(p) && chunks.find(p) == chunks.end()) {
                                    handle.first(std::make_pair(it->first, p), it->second, -dist2);
                                    lazyArea = false;
                                }
                            }
                        }}}
                    }
                    if(!lazyArea)
                        loadQueue.notify_all();
                }
                allLazy &= lazyArea;
                if (lazyArea) { // Clear un-used regions
                    ZoneScopedN("Region");
                    const auto unique = it->second->getRegions(); // MAYBE: shared then unique
#if TRACY_ENABLE
                    region_count += unique->size();
#endif
                    for (auto it_r = unique->begin(); it_r != unique->end(); ++it_r) {
                        if([&] {
                            const auto keepDist = glm::pow2(keepDistance + REGION_LENGTH * 2);
                            for(const auto& diff: inAreaPlayers) {
                                if (glm::length2(diff - glm::lvec3(it_r->first) * glm::lvec3(REGION_LENGTH)) <= keepDist)
                                    return false;
                            }
                            return true;
                        }()) {
                            unique->erase(it_r); //FIXME: may wait for os file access (long)
                            break; //NOTE: save one only max per frame
                        }
                    }
                }
                ++it;
            }
        }
#if TRACY_ENABLE
        TracyPlot("ChunkCount", static_cast<int64_t>(chunk_count));
        if(allLazy) {
            TracyPlot("Region", static_cast<int64_t>(region_count));
        }
        TracyPlot("ChunkLoad", static_cast<int64_t>(loadQueue.size()));
        TracyPlot("ChunkUnload", static_cast<int64_t>(saveQueue.size()));
#endif
    }
    { // Update entities
        ZoneScopedN("Entities");
        size_t item_count = 0;
        entities.remove([&](entity_id type, Entity &val) {
            //TODO: find mass center
            const auto OFFSET = glm::vec3(val.size) * val.scale / 2.f;
            const auto DIST2 = glm::pow2(loadDistance); //NOTE: must be < keepDistance + glm::length(OFFSET) to avoid collision miss
            val.instances.remove([&](entity_id, Entity::Instance &inst) {
                if (type == PLAYER_ENTITY_ID) {
                    //MAYBE: update players ?
                    item_count++;
                    return false;
                }

                //TODO: partition space
                auto pos = inst.pos + OFFSET;
                for(const auto& area: areas) {
                    //NOTE: consider entity mass negligible
                    inst.velocity += area.second->getGravity((pos - area.second->getOffset()).as_voxel()) * deltaTime;
                }
                //MAYBE: friction
                if (move(pos, inst.velocity, 1, glm::max_axis(OFFSET))) {
                    inst.velocity = glm::vec3(0);
                }
                inst.pos = pos - OFFSET;
                if (entities.at(PLAYER_ENTITY_ID).instances.contains([&](const entity_id&, const Entity::Instance& player) {
                    return glm::length2(glm::divide((player.pos - inst.pos).as_voxel())) <= DIST2;
                })) {
                    item_count++;
                    return false;
                }
                //TODO: Save to files if !temporary
                return true;
            });
            return !val.permanant && val.instances.empty();
        });
        TracyPlot("EntityCount", static_cast<int64_t>(item_count));

        constexpr auto CAT_SIZE = sizeof(entity_id::index) + sizeof(size_t);
        constexpr auto ITEM_SIZE = sizeof(entity_id::index) + sizeof(glm::ifvec3) + sizeof(glm::vec3);
        //TODO: only moved (pos, vel modified)
        //TODO: reduce rate with distance
        auto packet = net::PacketWriter(net::server_packet_type::ENTITIES, CAT_SIZE * entities.size() + ITEM_SIZE * item_count);
        entities.iter([&](entity_id id, const Entity &entity) {
            packet.write(id.index);
            packet.write(entity.instances.size());
            entity.instances.iter([&](entity_id i, const Entity::Instance &inst) {
                packet.write(i.index);
                packet.write(inst.pos);
                packet.write(inst.velocity);
            });
        });
        host.sendBroadcast(packet.finish(), net::server::queue::ENTITY, 1);
    }

    { // Store loaded chunks
        ZoneScopedN("Load");
        robin_hood::pair<area_<chunk_pos>, std::shared_ptr<Chunk>> loaded;
        for (auto handle = loadedQueue.extractor(); handle.first(loaded);) {
            if (const auto it = areas.find(loaded.first.first); it != areas.end()) {
                it->second->setChunks().emplace(loaded.first.second, loaded.second);
                const auto areaOffset = glm::divide(it->second->getOffset().as_voxel());
                loadChunk(loaded.first, areaOffset, it->second->getChunks());
            }
        }
    }
}

std::optional<uint16_t> Universe::onConnect(net::server::Peer* peer) {
    ZoneScopedN("Connect");
    using namespace net;
    LOG_I("Client connect from " << peer->getAddress());
    net_client* client = new net_client(entities.at(PLAYER_ENTITY_ID).instances.emplace(Entity::Instance{spawnPoint, glm::vec3(0)}));
    peer->ctx = client;

    {
        auto packet = PacketWriter(server_packet_type::CAPABILITIES, sizeof(loadDistance) + sizeof(bool) + sizeof(floodFillLimit) * PREDICTABLE);
        packet.write(loadDistance);
        packet.write(PREDICTABLE);
        if constexpr (PREDICTABLE) {
            packet.write(floodFillLimit);
        }
        peer->send(packet.finish());
    }

    //TODO: lock while not received
    peer->send(data::out_buffer(data::out_view((uint8_t*)dict_content.data(), dict_content.size()), nullptr), net::server::queue::CHUNK);
    {
        auto player = findEntity(PLAYER_ENTITY_ID, client->instanceId);
        auto packet = PacketWriter(server_packet_type::TELEPORT, sizeof(size_t) + sizeof(voxel_pos));
        packet.write(client->instanceId.index);
        packet.write(player->pos.as_voxel());
        peer->send(packet.finish());
        movedPlayers.insert(client->instanceId);
    }
    broadcastEntities();
    broadcastMessage("> Player" + std::to_string(client->instanceId.index) + " has joined us");
    broadcastAreas();
    return std::nullopt;
}
bool Universe::onDisconnect(net::server::Peer *peer, bool is_app, uint16_t reason) {
    ZoneScopedN("Disconnect");
    LOG_I((is_app ? "Client disconnect from " : "Client connection lost from ") << peer->getAddress() << " (" << reason << ')');
    if (auto data = peer->getCtx<net_client>()) {
        broadcastMessage("> Player" + std::to_string(data->instanceId.index) + " has left our universe");
        entities.at(PLAYER_ENTITY_ID).instances.free(data->instanceId);
        delete data;
        peer->ctx = nullptr;
    }
    return true;
}

bool Universe::onPacket(net::server::Peer *peer, const data::out_view &buf, net::PacketFlags) {
    ZoneScopedN("Packet");

    using namespace net;

    auto packet = PacketReader(buf);
    client_packet_type type;
    if(!packet.read(type)) {
        LOG_D("Empty packet from " << peer->getAddress());
        return false;
    }

    switch (type) {
        case client_packet_type::CAPABILITIES: {
            auto data = peer->getCtx<net_client>();
            packet.read(data->handleEdits);
            if (!PREDICTABLE && data->handleEdits) {
                LOG_E("Client misread capabilities");
            }
            break;
        }
        case client_packet_type::MOVE: {
            auto data = peer->getCtx<net_client>();
            if (voxel_pos pos; !packet.read(pos) ||
                !movePlayer(data->instanceId, pos)) {
                LOG_T("Bad move");
            }
            break;
        }
        case client_packet_type::FILL_SHAPE: {
            if(const auto fill = packet.read<world::action::FillShape>()) {
                //TODO: check ray
                if (fill->val.is_solid() && !isAreaFree(fill->pos, world::action::ToGeometry(fill->shape), fill->radius)) {
                    LOG_T("Entity in solid fill area");
                    break;
                }
                host.iterPeers([&](net::server::Peer *peer) {
                    auto data = peer->getCtx<net_client>();
                    //MAYBE: only in range
                    if (data && data->handleEdits)
                        data->pendingEdits.push(*fill);
                });
                set(fill->pos, fill->radius, fill->shape, fill->val);
                //TODO: handle inventory
            } else {
                LOG_T("Bad fill");
            }
            break;
        }
        case client_packet_type::MESSAGE: {
            const auto ref = packet.readRemaining();
            broadcastMessage("Player" + std::to_string(peer->getCtx<net_client>()->instanceId.index)
                + ": " + std::string((const char*)ref.data(), ref.size()));
            break;
        }
        case client_packet_type::MISSING_REGIONS: {
            auto data = peer->getCtx<net_client>();
            if (auto player = findEntity(PLAYER_ENTITY_ID, data->instanceId)) {
                const auto pos = player->pos.as_voxel();

                area_id id;
                if (!packet.read<area_id>(id))
                    break;
                if (auto area = areas.find(id); area != areas.end()) {
                    const chunk_pos areaOffset = glm::divide(pos - area->second->getOffset().as_voxel());
                    while (!packet.isDone()) {
                        region_pos rpos;
                        if (!packet.read(rpos))
                            break;

                        if (auto it_r = area->second->getRegions()->find(rpos); it_r != area->second->getRegions()->end()) {
                            if (glm::length2(areaOffset - glm::lvec3(it_r->first) * glm::lvec3(REGION_LENGTH)) <= glm::pow2(loadDistance + REGION_LENGTH * 2)) {
                                auto packet = PacketWriter(server_packet_type::REGION, sizeof(area_<region_pos>));
                                packet.write<area_<region_pos>>(std::make_pair(id, rpos));
                                packet.pushPart([&](PacketWriter::part part) {
                                    it_r->second->averages(part); });
                                peer->send(packet.finish(), net::server::CHUNK);
                            } else {
                                LOG_T("Request out of range region");
                            }

                        }
                    }
                } else {
                    LOG_T("Bad region request");
                }
            }
            break;
        }
        case client_packet_type::MISSING_CHUNKS: {
            auto data = peer->getCtx<net_client>();
            if (auto player = findEntity(PLAYER_ENTITY_ID, data->instanceId )) {
                const auto pos = player->pos.as_voxel();

                area_id id;
                if (!packet.read<area_id>(id))
                    break;
                if (auto area = areas.find(id); area != areas.end()) {
                    auto &chunks = area->second->getChunks();
                    const chunk_pos areaOffset = glm::divide(pos - area->second->getOffset().as_voxel());

                    for (size_t i = 0; !packet.isDone() && i < MAX_PENDING_CHUNK_COUNT; i++) {
                        chunk_pos cpos;
                        if (!packet.read(cpos))
                            break;
                        const auto dist = glm::length2(areaOffset - cpos);
                        if (dist <= glm::pow2(loadDistance) && chunks.inRange(cpos)) {
                            if (chunks.findInRange(cpos).has_value())
                                data->pushChunk(std::make_pair(id, cpos), dist);
                        } else {
                            LOG_T("Request out of range chunk");
                        }
                    }
                } else {
                    LOG_T("Bad chunk request");
                }
            }
            break;
        }
        case client_packet_type::MISSING_ENTITY: {
            size_t id;
            if (!packet.read(id))
                break;

            if (auto entity = entities.directly_at(id)) {
                if (const auto area = std::get_if<Entity::area_t>(&entity->shape)) {
                    auto packet = PacketWriter(server_packet_type::ENTITY_SHAPE, sizeof(id) + sizeof(bool) + sizeof(size_t));
                    packet.write(id);
                    packet.write(true);
                    for (auto it = area->begin(); it != area->end(); ++it) {
                        std::ostringstream out;
                        std::dynamic_pointer_cast<Chunk>(*it)->write(out);
                        packet.pushSizedPart([&](PacketWriter::part part) {
                            dict_write_ctx.compress(out.str(), part); });
                    }
                    peer->send(packet.finish(), net::server::CHUNK);
                } else {
                    const auto model = std::get_if<Entity::model_t>(&entity->shape);
                    assert(model);
                    auto packet = PacketWriter(server_packet_type::ENTITY_SHAPE, sizeof(id) + sizeof(bool) + model->size());
                    //MAYBE: prefix model with id+false
                    packet.write(id);
                    packet.write(false);
                    packet.write(model->data(), model->size());
                    peer->send(packet.finish(), net::server::CHUNK);
                }
            } else {
                LOG_T("Bad entity request " << id);
            }
            break;
        }
        default:
            LOG_T("Bad packet from " << peer->getAddress());
            break;
    }
    return true;
}
void Universe::broadcastAreas() {
    constexpr size_t ITEM_SIZE = sizeof(area_id) + sizeof(world::Area::params);

    auto packet = net::PacketWriter(net::server_packet_type::AREAS, ITEM_SIZE * areas.size());
    for(const auto& area: areas) {
        const auto params = area.second->getParams();
        packet.write(area.first);
        packet.write(world::Area::params{params.center, params.radius, area.second->getCurvature()});
    }
    host.sendBroadcast(packet.finish());
}
data::out_buffer Universe::serializeChunk(area_<chunk_pos> id, const std::shared_ptr<Chunk> &data) {
    ZoneScopedN("Chunk");
    std::ostringstream out;
    data->write(out);
    auto packet = net::PacketWriter(net::server_packet_type::CHUNK, sizeof(id));
    packet.write(id);
    packet.pushPart([&](net::PacketWriter::part part){
        dict_write_ctx.compress(out.str(), part); });
    return packet.finish();
}
void Universe::broadcastMessage(const std::string& text) {
    host.sendBroadcast(net::PacketWriter::Of(net::server_packet_type::MESSAGE, text.data(), text.size()));
}
void Universe::broadcastEntities() {
    constexpr auto ITEM_SIZE = sizeof(entity_id::index) + sizeof(glm::usvec3) + sizeof(glm::vec3) + sizeof(uint8_t);
    auto packet = net::PacketWriter(net::server_packet_type::ENTITY_TYPES, ITEM_SIZE * entities.size());
    entities.iter([&](entity_id id, const Entity &entity) {
        packet.write(id.index);
        packet.write(entity.size);
        packet.write(entity.scale);
        uint8_t flags = 0;
        if (entity.permanant)
            flags |= 1;
        if (std::holds_alternative<Entity::area_t>(entity.shape))
            flags |= 2;
        packet.write(flags);
    });
    host.sendBroadcast(packet.finish(), net::server::queue::ENTITY);
}

void Universe::updateChunk(area_map::iterator &, world::ChunkContainer::iterator &, chunk_pos, float /*deltaTime*/) {}
void Universe::loadChunk(area_<chunk_pos>, chunk_pos, const world::ChunkContainer &) {}

void Universe::setOptions(const Universe::options& options) {
    loadDistance = options.loadDistance;
    keepDistance = options.keepDistance;
    floodFillLimit = options.floodFillLimit;
}

Universe::ray_result Universe::raycast(const geometry::Ray &ray) const {
    return Raycast(ray, areas);
}

bool Universe::isAreaFree(const area_<voxel_pos> &pos, const geometry::Shape shape, const uint16_t radius) const {
    if (const auto it = areas.find(pos.first); it != areas.end()) {
        const auto center = pos.second + it->second->getOffset().as_voxel();
        return !entities.contains([&](entity_id, const Entity &entity) {
            return entity.instances.contains([&](entity_id, const Entity::Instance &inst) {
                return geometry::InShape(shape, center, radius, inst.pos.as_voxel(), entity.size);
            });
        });
    } else
        return false;
}

world::ItemList Universe::set(const area_<voxel_pos>& pos, int radius, world::action::Shape shape, const world::Voxel& val) {
    ZoneScopedN("Fill");
    ItemList list;
    const bool stupidClient = host.anyPeer([&](net::server::Peer *peer) {
        auto data = peer->getCtx<net_client>();
        return data && !data->handleEdits;
    });
    robin_hood::unordered_map<chunk_pos, std::vector<Chunk::Edit>> edits;
    const auto fill = world::action::FillShape(pos, val, shape, radius);
    world::iterator::Apply<Chunk>(areas, fill,
        [&](std::shared_ptr<Chunk>& ck, chunk_pos ck_pos, chunk_voxel_idx idx, Voxel /*prev*/, Voxel next, float delay) {
            if (stupidClient)
                edits[ck_pos].push_back(Chunk::Edit{next, delay, idx});
            //TODO: apply break table
            //TODO: inventory
            ck->replace(idx, next, delay);
    });
    if (world::iterator::Split<Chunk>(areas, fill, floodFillLimit, [&](const robin_hood::unordered_set<voxel_pos>& part, Voxel cleanVoxel,
        world::ChunkContainer& chunks, std::shared_ptr<Chunk>& ck, chunk_pos& ck_pos, std::shared_ptr<Area>& it
    ){
        voxel_pos min = voxel_pos(INT64_MAX);
        voxel_pos max = voxel_pos(INT64_MIN);
        for(auto full: part) {
            min = glm::min<3, long long>(min, full);
            max = glm::max<3, long long>(max, full);
        }
        const auto size = max - min;
        const chunk_pos scale = chunk_pos(1) + glm::divide(size);
        const auto type = entities.emplace(size, glm::vec3(1), false);
        auto &entity = entities.at(type);
        auto &area = std::get<Entity::area_t>(entity.shape);
        {
            const auto chunk_count = scale.x * scale.y * scale.z;
            area.reserve(chunk_count);
            for (long i = 0; i < chunk_count; i++)
                area.push_back(createChunk());
        }
        std::shared_ptr<Chunk> ck_dest = nullptr;
        chunk_pos ck_pos_dest = chunk_pos(INT32_MAX);
        for(auto full: part) {
            const auto split = glm::splitIdx(full);
            if (Voxel v; world::iterator::GetChunk<Chunk>(chunks, split, v, ck, ck_pos) && v.is_solid()) {
                if (stupidClient)
                    edits[ck_pos].push_back(Chunk::Edit{cleanVoxel, fill.radius * .05f, split.second});

                ck->replace(split.second, cleanVoxel);
                const auto spl = glm::splitIdx(full - min);
                if (spl.first != ck_pos_dest) {
                    ck_dest = std::dynamic_pointer_cast<Chunk>(area.at(glm::toIdx(spl.first, scale)));
                    assert(ck_dest);
                    ck_pos_dest = spl.first;
                }
                ck_dest->set(spl.second, v);
            }
        }
        entity.instances.emplace(Entity::Instance{it->getOffset() + min, glm::vec3(0)});
    })) { broadcastEntities(); }

    if (stupidClient && !edits.empty()) {
        ZoneScopedN("Packet");
        size_t size = sizeof(area_id);
        for(const auto& part: edits) {
            size += sizeof(chunk_pos);
            size += sizeof(chunk_voxel_idx);
            size += sizeof(Chunk::Edit) * part.second.size();
        }
        auto packet = net::PacketWriter(net::server_packet_type::EDITS, size);
        packet.write(pos.first);
        for(const auto& part: edits) {
            packet.write(part.first);
            packet.write<chunk_voxel_idx>(part.second.size());
            packet.write(part.second.data(), part.second.size() * sizeof(Chunk::Edit));
        }
        auto buffer = packet.finish();
        host.iterPeers([&](net::server::Peer *peer) {
            //MAYBE: only in range
            auto data = peer->getCtx<net_client>();
            if (data && !data->handleEdits)
                peer->send(buffer, net::server::queue::CHUNK);
        });
    }
    return list;
}

bool Universe::collide_end(const glm::ifvec3 &pos, const glm::vec3 &vel, int density, float radius) const {
    return std::holds_alternative<ray_target>(raycast(geometry::Ray((pos + vel) * density, vel, radius)));
}
bool Universe::collide_point(const glm::ifvec3 &pos, const glm::vec3 &vel, int density) const {
    const auto target = ((pos + vel) * density).as_voxel();
    for(auto& area: areas) {
        if(area.second->getBounding().contains(target)) {
            const auto &offset = area.second->getOffset().as_voxel();
            const auto &chunks = area.second->getChunks();
            const auto pos = target - offset;
            const chunk_pos cPos = glm::divide(pos);
            if (const auto it = chunks.find(cPos); it != chunks.end()) {
                const auto voxel = it->second->getAt(glm::modulo(pos));
                if (voxel.is_solid()) {
                    return true;
                }
            } else if(chunks.inRange(cPos)) {
                return true;
            }
        }
    }
    return false;
}

entity_instance_id Universe::addEntity(entity_id type, const Entity::Instance &instance) {
    return std::make_pair(type, entities.at(type).instances.push(instance));
}

Universe::Entity::Instance* Universe::findEntity(entity_id type, entity_id id) {
    if(!entities.contains(type))
        return nullptr;

    if(!entities.at(type).instances.contains(id))
        return nullptr;

    return &entities.at(type).instances.at(id);
}

bool Universe::movePlayer(data::generational::id id, glm::ifvec3 pos) {
    if (auto player = findEntity(PLAYER_ENTITY_ID, id)) {
        const auto initialPos = player->pos.as_voxel();
        if (initialPos == pos.as_voxel())
            return true;

        //TODO: check dist + collision from a to b
        movedPlayers.insert(id);
        player->pos = pos;
        return true;
    } else
        return false;
}

std::shared_ptr<Chunk> Universe::createChunk(const chunk_pos &pos, const std::unique_ptr<world::generator::Abstract> &rnd) const {
    return std::make_shared<Chunk>(pos, rnd);
}
std::shared_ptr<Chunk> Universe::createChunk(std::istream &str) const {
    return std::make_shared<Chunk>(str);
}
std::shared_ptr<Chunk> Universe::createChunk() const {
    return std::make_shared<Chunk>();
}