#include "Connection.hpp"
#include "World.hpp"

#include <optional>
#include <algorithm>
#include <boost/asio.hpp>



namespace Connection {
asio::awaitable<Packet> Client::receivePacket() {
    Packet packet;
    // Receive operation
    packet.op = co_await receiveProtobuf<Generic::Operation>();
    // Receive data
    switch (packet.op.code()) {
    case Generic::Operation::Error: {
        packet.data = std::make_unique<Generic::Error>(co_await receiveProtobuf<Generic::Error>());
    } break;
    case Generic::Operation::SimpleAuth: {
        packet.data = std::make_unique<Generic::SimpleAuth>(co_await receiveProtobuf<Generic::SimpleAuth>(30));
    } break;
    case Generic::Operation::RobotUpdate: {
        packet.data = std::make_unique<Generic::Robot>(co_await receiveProtobuf<Generic::Robot>());
    } break;
    case Generic::Operation::UserInfoSync:
    case Generic::Operation::MakeRoom:
    case Generic::Operation::JoinRandomRoom:
    case Generic::Operation::Disconnect:
    case Generic::Operation::OK:
        break;
    default: throw FatalError("Invalid opcode: "+std::to_string(packet.op.code()), Generic::Error::InvalidOpcode);
    }
}

asio::awaitable<void> Client::sendProtobuf(const google::protobuf::Message& buffer) {
    // Send size
    uint16_t size = buffer.ByteSizeLong();
    co_await asio::async_write(socket, asio::buffer(&size, sizeof(size)), asio::use_awaitable);
    // Send buffer
    std::vector<char> rawBuffer(size);
    buffer.SerializeToArray(rawBuffer.data(), rawBuffer.size());
    co_await asio::async_write(socket, asio::buffer(rawBuffer.data(), rawBuffer.size()), asio::use_awaitable);
}

asio::awaitable<void> Client::sendPacket(const Packet& packet) {
    // Send operation
    co_await sendProtobuf(packet.op);
    // Send data
    if (packet.data) {
        co_await sendProtobuf(*packet.data);
    }
}

asio::awaitable<void> Client::handlePacket(const Packet& packet) {
    // Check if authentication is required and completed
    if (packet.op.code() >= Generic::Operation::_MinAuth && !isAuthed()) {
        throw FatalError("Authentication required", Generic::Error::AuthenticationRequired);
    }
    // Handle the packet
    switch (packet.op.code()) {
    case Generic::Operation::Disconnect: {
        good = 0;
    } break;
    case Generic::Operation::SimpleAuth: {
        const Generic::SimpleAuth& authData = *static_cast<Generic::SimpleAuth*>(packet.data.get());
        // Check that username is not already taken
        if (std::find_if(global.clients.begin(), global.clients.end(), [&, this] (const auto& o) {return o->userInfo.publicinfo().name() == authData.username();}) != global.clients.end()) {
            throw Error("Username has already been taken", Generic::Error::UsernameAlreadyTaken);
        }
        // Check that username is "good" (no unprintable characters and stuff like that)
        //TODO...
        // Set username
        userInfo.mutable_publicinfo()->set_name(std::move(authData.username()));
        // Set temporary user and client id
        auto userRef = userInfo.mutable_publicinfo()->mutable_reference();
        userRef->set_userid(global.getTemporaryId());
        userRef->set_clientid(global.getTemporaryId());
        // Mark as authed
        authed = true;
        // Send user info
        Packet userInfoSync;
        userInfoSync.op.set_code(Generic::Operation::UserInfoSync);
        userInfoSync.data = std::make_unique<Generic::UserInfo>(userInfo);
    } break;
    case Generic::Operation::MakeRoom: {
        // Create room
        auto room = std::make_shared<World::Room>(global);
        global.rooms.push_back(room);
        co_await room->addClient(shared_from_this());
    } break;
    case Generic::Operation::JoinRandomRoom: {
        // Get random room
        auto room = global.getRandomRoom();
        // Check that room was found
        if (!room) {
            throw Error("No available room could be found", Generic::Error::RoomNotAvailable);
        }
        // Add client to it
        co_await room->addClient(shared_from_this());
    } break;
    case Generic::Operation::LeaveRoom: {
        // Check that client is in any room
        if (!currentRoom) {
            throw Error("You are not in any room", Generic::Error::IllegalOperation);
        }
        // Remove client from room
        co_await currentRoom->removeClient(shared_from_this());
    } break;
    default: {}
    }
    // Send OK
    co_await sendOK();
}

asio::awaitable<void> Client::connect() {
    // Send initial "OK"
    try {
        co_await sendOK();
    } catch (...) {
        good = false;
    }
    // Run while connection is good
    while (good) {
        std::optional<Error> error;
        try {
            co_await handlePacket(co_await receivePacket());
        } catch (FatalError& e) {
            error = std::move(e);
            good = false;
        } catch (Error& e) {
            error = std::move(e);
        } catch (std::exception& e) {
            error = Error("Internal server error: "+std::string(e.what()), Generic::Error::Unknown);
            good = false;
        }
        if (error.has_value()) {
            co_await sendPacket(error.value().packet);
        }
    }
    // Try to send disconnect
    try {
        Packet packet;
        packet.op.set_code(Generic::Operation::Disconnect);
        co_await sendPacket(packet);
    } catch (...) {}
    // Close socket
    boost::system::error_code ec;
    socket.close(ec);
}

std::shared_ptr<World::Room> Global::getRandomRoom() const {
    //TODO: Real rng
    for (const auto& room : rooms) {
        if (room->isOpen()) {
            return room;
        }
    }
    return nullptr;
}

asio::awaitable<void> Server::listen(int port) {
    try {
        // Create acceptor
        asio::ip::tcp::acceptor acceptor(ioc, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port));
        // Handle all incoming connections
        while (true) {
            auto client = std::make_shared<Client>(ioc, global);
            co_await acceptor.async_accept(client->getSocket(), asio::use_awaitable);
            asio::co_spawn(ioc, client->connect(), asio::detached);
            global.clients.push_back(std::move(client));
        }
    } catch (std::exception& e) {
        std::cerr << "Failed to stay alive: " << e.what() << std::endl;
    }
}
}