#include "./Server.hpp"
#include "world/Universe.hpp"
#include <signal.h>
#include <chrono>
#include <thread>
#include <Tracy.hpp>
#include "./world/client.hpp"

#undef LOG_PREFIX
#define LOG_PREFIX "Server: "

#ifndef STANDALONE_SERVER
constexpr bool CAN_SHARE = true;
#else
constexpr bool CAN_SHARE = false;
#endif

using namespace world;
namespace ws = world::server;

Server::Server(config::server::options& options): 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 memory::read_view &buf, net::PacketFlags flags) { return onPacket(peer, buf, flags); }
    ), serializer(options.world.folderPath),
    localHandle(CAN_SHARE && options.allowLocal ? new server_handle() : nullptr) { }
Server::~Server() {
    LOG_W("Stopped");
}

static bool running = true;
void handle_signal(int) { running = false; }

void Server::run() {
    signal(SIGINT, handle_signal);
    signal(SIGTERM, handle_signal);

    universe = std::make_unique<ws::Universe>(options.world, &serializer, localHandle);
    if (localHandle) {
        localHandle->emit = [&](const world::action::packet &packet) { onPacket(packet); };
        localHandle->entityId = universe->addPlayer();
        {
            const auto entry = universe->findHierarchy(localHandle->entityId); assert(entry);
            localHandle->teleport = world::relative_transform{entry->parent, entry->relative};
        }
        localHandle->running = true;
    }

    auto lastUpdate = std::chrono::steady_clock::now();
    const auto deltaTime = [&](const std::chrono::_V2::steady_clock::time_point &now) {
        std::chrono::duration<float, std::ratio<1>> delta = now - lastUpdate;
        lastUpdate = now;
        return delta.count();
    };
    while (running && (!localHandle || localHandle->running)) {
        FrameMarkStart("Server");
        const auto tickStart = std::chrono::steady_clock::now();
        universe->update(deltaTime(tickStart));
        universe->upgrade();
        broadcastEntitiesChanges(*universe->setElements());
        {
            size_t upt = 1;
            const size_t UPT = options.upt;
            const auto nextTick = tickStart + std::chrono::milliseconds(1000 / options.tps);
            const auto uptDelay = std::chrono::milliseconds(1000 / options.tps / UPT);
            do {
                pull();
                const auto now = std::chrono::steady_clock::now();
                if (now >= nextTick)
                    break;
                if (upt < UPT && now >= lastUpdate + uptDelay) {
                    universe->update(deltaTime(now));
                    upt++;
                }
                std::this_thread::sleep_for(std::chrono::milliseconds(1));
            } while (true);
        }
        FrameMarkEnd("Server");
    }
    broadcastMessage("> Server is closing");
    host.sendBroadcast(memory::read_buffer::Of(net::server_packet_type::QUIT, net::disconnect_reason::CLOSE));
    for (size_t i = 0; i < 20; i++) {
        std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 20));
        host.pull();
    }
    host.close((uint16_t)net::disconnect_reason::CLOSE);
    if (localHandle) {
        universe->removePlayer(localHandle->entityId);
    }
    universe.reset();
}

void Server::pull() {
    ZoneScopedN("Network");
    host.pull();
    // Send pending data
    host.iterPeers([&](net::server::Peer *peer) {
        auto ctx = peer->getCtx<ws::client>();
        if (!ctx)
            return;

        auto& edits = ctx->pending.edits;
        constexpr auto PARALLEL_EDITS = 1;
        if (!edits.empty() && peer->queueSize(net::server::queue::EDIT) <= PARALLEL_EDITS - 1) {
            peer->send(memory::read_buffer::Of(net::server_packet_type::EDITS, edits.front()));
            edits.pop();
        }

        auto& chunks = ctx->pending.chunks;
        if (!chunks.empty()) {
            //TODO: use congestion
            constexpr auto PARALLEL_CHUNKS = 5;
            size_t i = peer->queueSize(net::server::queue::CHUNK);
            node_chunk_pos pending{generational::id(), chunk_pos(0)};
            while(i <= (PARALLEL_CHUNKS - 1) && chunks.pop(pending)) {
                if (const auto chunk = universe->findChunk(pending)) {
                    peer->send(serializer.writeChunk(pending, chunk), net::server::queue::CHUNK);
                    i++;
                }
            }

            if (chunks.empty())
                peer->send(serializer.writeChunkReset()); //FIXME: must be received after last chunk
        }
    });
}

bool Server::onConnect(net::server::Peer* peer) {
    ZoneScopedN("Connect");
    LOG_I("Client connect from " << peer->getAddress());
    return true;
}
bool Server::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 ctx = peer->getCtx<ws::client>()) {
        //FIXME: save inventory, pos...
        universe->removePlayer(ctx->entityId);
        broadcastMessage("> " + ctx->name + " has left our universe");
        delete ctx;
        peer->ctx = nullptr;
    }
    return true;
}

bool Server::onPacket(net::server::Peer *peer, const memory::read_view &buf, net::PacketFlags) {
    ZoneScopedN("Packet");

    using namespace net;

    auto packet = memory::read_buffer(buf, nullptr);

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

    const auto ctx = peer->getCtx<ws::client>();
    if (!ctx) { // Not logged in
        //TODO: disconnect unauthorized
        if (type != net::client_packet_type::HELLO)
            return false;

        uint8_t type;
        if (!packet.read(type))
            return false;

        switch (type) {
        case 0: { // Register
            //TODO: check player index and get id
            const auto rem = packet.readRemaining();
            std::string name((const char*)rem.data());
            if (name.size() == 0 || (name.size() != rem.size() && name.size() != rem.size()-1))
                return false;

            peer->ctx = new ws::client(0, name, universe->addPlayer());
            break;
        }

        //TODO: handle login
        default:
            return false;
        }

        peer->send(serializer.writeCapabilities(options.world.loadDistance, options.world.floodFillLimit));
        //TODO: lock while not received
        peer->send(serializer.writeDict(), net::server::queue::CHUNK);
        const auto ctx = peer->getCtx<ws::client>();
        {
            const auto entry = universe->findHierarchy(ctx->entityId); assert(entry);
            peer->send(serializer.writeTeleport(ctx->entityId, world::relative_transform{entry->parent, entry->relative}));
        }
        broadcastMessage("> " + ctx->name + " has joined us");
        if (const auto entities = serializeEntitiesFrom(*universe->getElements()); entities.ptr) {
            peer->send(entities, net::server::queue::ENTITY);
        }
        return true;
    }

    switch (type) {
        case client_packet_type::CAPABILITIES: {
            packet.read(ctx->caps.handleEdits);
            /*if (!PREDICTABLE && ctx->handleEdits) {
                LOG_E("Client misread capabilities");
            }*/
            break;
        }
        case client_packet_type::MOVE: {
            if (relative_pos pos; !packet.read(pos) ||
                !universe->movePlayer(ctx->entityId, pos)) {
                LOG_T("Bad move");
                const auto entry = universe->findHierarchy(ctx->entityId); assert(entry);
                peer->send(serializer.writeTeleport(ctx->entityId, world::relative_transform{entry->parent, entry->relative}));
            }
            break;
        }
        case client_packet_type::FILL_SHAPE: {
            if(const auto fill = packet.read<action::FillShape>()) {
                fillShape(*fill);
            } else {
                LOG_T("Bad fill");
            }
            break;
        }
        case client_packet_type::MESSAGE: {
            const auto ref = packet.readRemaining();
            broadcastMessage(ctx->name + ": " + std::string((const char*)ref.data(), ref.size()));
            break;
        }
        case client_packet_type::REQUEST_REGIONS: {
            if (auto player = universe->getPlayer(ctx->entityId)) {
                area_id id;
                if (!packet.read(id))
                    break;
                if (auto node = universe->getElements()->findArea(id)) {
                    region_pos rpos;
                    auto regions = node->get()->getRegions();
                    const auto pos = player->absolute.position;
                    while (!packet.isDone() && packet.read(rpos)) {
                        if (auto it_r = regions->find(rpos); it_r != regions->end()) {
                            const auto off = glm::divide(node->absolute.computeChild(glm::multiply(it_r->first, glm::lvec3(REGION_LENGTH * CHUNK_LENGTH))) - pos);
                            if (glm::length2(off) <= glm::pow2(options.world.loadDistance + REGION_LENGTH * 2)) {
                                peer->send(serializer.writeRegion(area_region_ref(id, rpos), it_r->second), net::server::CHUNK);
                            } else {
                                LOG_T("Request out of range region");
                            }
                        } else {
                            LOG_T("Requested region not found");
                        }
                    }
                } else {
                    LOG_T("Bad region request");
                }
            }
            break;
        }
        case client_packet_type::REQUEST_CHUNKS: {
            if (auto player = universe->getPlayer(ctx->entityId)) {
                const auto pos = player->absolute.position;

                node_id id;
                if (!packet.read<node_id>(id))
                    break;

                const auto elements = universe->getElements();
                if (ws::Elements::Is<ws::Elements::Type::Area>(elements->withFlag(id))) {
                    if (auto area = elements->findArea(id.val)) {
                        auto &chunks = area->get()->getChunks();
                        chunk_pos cpos;
                        for (size_t i = 0; !packet.isDone() && i < MAX_PENDING_CHUNK_COUNT && packet.read(cpos); i++) {
                            const auto dist = glm::length2(glm::divide(area->absolute.computeChild(glm::multiply(cpos)) - pos));
                            if (dist <= glm::pow2(options.world.loadDistance) && chunks.inRange(cpos)) {
                                if (chunks.findInRange(cpos) != nullptr)
                                    ctx->pending.chunks.push(node_chunk_pos{id, cpos}, dist);
                                //MAYBE: else notify client
                            } else {
                                LOG_T("Request out of range chunk");
                            }
                        }
                    } else {
                        LOG_T("Bad chunk request");
                    }
                } else if (auto part = elements->findPart(id.val)) {
                    auto size = part->get();
                    const auto dist = glm::length2(glm::divide(part->absolute.position - pos));
                    if (dist <= glm::pow2(options.world.loadDistance)) {
                        chunk_pos cpos;
                        for (size_t i = 0; !packet.isDone() && i < MAX_PENDING_CHUNK_COUNT && packet.read(cpos); i++) {
                            if (dist <= glm::pow2(options.world.loadDistance) && size->inRange(cpos)) {
                                ctx->pending.chunks.push(node_chunk_pos{id, cpos}, dist);
                            } else {
                                LOG_T("Request out of range chunk");
                            }
                        }
                    } else {
                        LOG_T("Request out of range part");
                    }
                } else {
                    LOG_T("Bad chunk request");
                }
                if (ctx->pending.chunks.empty())
                    peer->send(serializer.writeChunkReset());
            }
            break;
        }
        case client_packet_type::REQUEST_ENTITIES: {
            node_id root;
            if (!packet.read(root))
                break;

            LOG_E("Unimplemented");
            break;
        }
        default:
            LOG_T("Bad packet from " << peer->getAddress() << " " << (int)type);
            break;
    }
    return true;
}

void Server::onPacket(const world::action::packet &packet) {
    ZoneScopedN("Local Packet");
    assert(localHandle);

    if(const auto fill = std::get_if<world::action::FillShape>(&packet)) {
        fillShape(*fill);
    } else if(const auto message = std::get_if<world::action::Message>(&packet)) {
        broadcastMessage(localHandle->playerName + ": " + message->text);
    } else if(const auto move = std::get_if<world::action::Move>(&packet)) {
        if(!universe->movePlayer(localHandle->entityId, move->pos)) {
            LOG_W("Bad move of local player");
        }
    } else {
        LOG_W("Bad packet from local client " << packet.index());
    }
}

void Server::broadcastMessage(const std::string& msg) {
    if (CAN_SHARE && localHandle) {
        localHandle->messages.emplace(msg);
    }
    host.sendBroadcast(serializer.writerMessage(msg));
}

void Server::broadcastEntitiesChanges(world::server::Elements& l) {
    memory::write_buffer packet(net::server_packet_type::ENTITIES, sizeof(node_id));
    l.nodes.iter_empty([&] (const node_id& id) {
        if (Elements::Has<Elements::Flag::Removed>(id)) {
            const auto flagId = Elements::Set<Elements::Flag::Added, false>(
                Elements::Set<Elements::Flag::Moved, false>(id));
            packet.push(flagId);
            l.nodes.set_flag(Elements::Set<Elements::Flag::Removed, false>(id));
        }
    });
    l.hierarchy.iter([&](const Elements::hierarchy_entry &entry) {
        const auto id = l.withFlag(entry.self);
        assert(id);
        const auto added = Elements::Has<Elements::Flag::Added>(id);
        if (Elements::Has<Elements::Flag::Moved>(id) || added) {
            packet.push(id);
            packet.push(ws::Elements::start_point(entry.parent, entry.relative, entry.vel));
            l.nodes.set_flag(Elements::Set<Elements::Flag::Added, false>(
                Elements::Set<Elements::Flag::Moved, false>(id)));
        }
        if (added) {
            switch (Elements::GetType(id)) {
                case Elements::Type::Area: {
                    const auto area = l.findArea(id.val)->get();
                    packet.push(Area::params{area->getParams().radius, area->getCurvature()});
                    break;
                }
                case Elements::Type::Part:
                    packet.push(l.findPart(id.val)->get()->size);
                    break;

                case Elements::Type::Instance:
                    packet.push(NodeOf<Instance>::Make(l.findNode(id))->get()->type);
                    break;

                default:
                    break;
            }
        }
    });
    if (packet.isFull())
        host.sendBroadcast(packet.finish(), net::server::queue::ENTITY);

#if TRACY_ENABLE
    int64_t queued = 0;
    host.iterPeers([&](net::server::Peer *peer) { queued += peer->queueSize(net::server::queue::ENTITY); });
    TracyPlot("QueuedEntities", queued);
#endif
}

memory::read_buffer Server::serializeEntitiesFrom(const world::server::Elements& l, node_id root) {
    memory::write_buffer packet(net::server_packet_type::ENTITIES, sizeof(node_id));
    generational::id::idx_t from = 0;
    generational::id::idx_t count = l.hierarchy.size();
    if (const auto root_entry = l.hierarchy.get(root)) {
        for (node_id cur = root_entry->parent; cur;) { // Read parents in correct order
            const auto node = l.nodes.directly_at(cur);
            if (!node)
                break;

            const auto entry = l.hierarchy.get(cur);
            if (!entry)
                break;

            const auto flagCur = Elements::Set<Elements::Flag::Added, false>(
                Elements::Set<Elements::Flag::Moved, true>(cur));
            packet.push(flagCur);
            packet.push(ws::Elements::start_point{entry->parent, entry->relative, entry->vel});
            cur = entry->parent;
        }
        from = l.hierarchy.find(root).value().val;
        count = root_entry->childs;
    } else if (root) { // Not in hierarchy
        return memory::read_buffer(nullptr, 0);
    }

    for (size_t i = 0; i < count; i++) {
        const auto entry = l.hierarchy.at(from + i);
        assert(entry);
        const auto id = l.withFlag(entry->self);
        const auto node = l.nodes.directly_at(id);
        assert(node);
        const auto flagId = Elements::Set<Elements::Flag::Added, true>(
                Elements::Set<Elements::Flag::Moved, false>(id));
        packet.push(flagId);
        packet.push(ws::Elements::start_point(entry->parent, entry->relative, entry->vel));
        switch (Elements::GetType(id)) {
            case Elements::Type::Area: {
                const auto area = NodeOf<ws::Area>::Make(node)->get();
                packet.push(Area::params{area->getParams().radius, area->getCurvature()});
                break;
            }
            case Elements::Type::Part:
                packet.push(NodeOf<Part>::Make(node)->get()->size);
                break;

            case Elements::Type::Instance:
                packet.push(NodeOf<Instance>::Make(node)->get()->type);
                break;

            default:
                break;
        }
    }
    return packet.finish();
}

void Server::fillShape(const world::action::FillShape& fill) {
    //TODO: check ray and tool
    if (fill.val.is_solid() && !universe->isRangeFree(fill.pos, geometry::Volume{action::ToGeometry(fill.shape), fill.radius})) {
        LOG_T("Entity in solid fill area");
        return;
    }
    if (const auto id = universe->getElements()->withFlag(fill.pos.node); !Elements::Is<Elements::Type::Area>(id)) {
        LOG_W("Part edit not supported for now");
    }
    bool stupidClient = false;
    host.iterPeers([&](net::server::Peer *peer) {
        //MAYBE: only in range
        if (const auto ctx = peer->getCtx<ws::client>()) {
            if (ctx->caps.handleEdits)
                ctx->pending.edits.push(fill);
            else
                stupidClient = true;
        }
    });
    if (stupidClient) {
        ws::fill_edits edits;
        universe->fill(fill, &edits);
        if (!edits.empty()) {
            ZoneScopedN("Stupidity");
            auto buffer = serializer.writeEdits(fill.pos.node, edits);
            host.iterPeers([&](net::server::Peer *peer) {
                //MAYBE: only in range
                const auto ctx = peer->getCtx<ws::client>();
                if (ctx && !ctx->caps.handleEdits)
                    peer->send(buffer, net::server::queue::CHUNK);
            });
        }
    } else {
        universe->fill(fill);
    }
    //TODO: handle inventory
}