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

#include <string>
#include <chrono>



namespace World {
void fillSettings(Generic::Settings& settings) {
    if (!settings.has_maxdistanceperturn()) {
        settings.set_maxdistanceperturn(0.02f);
    }
    if (!settings.has_robotsperclient()) {
        settings.set_robotsperclient(50);
    }
    if (!settings.has_maxrobothealth()) {
        settings.set_maxrobothealth(20);
    }
    if (!settings.has_maxrobotdamage()) {
        settings.set_maxrobotdamage(4);
    }
    if (!settings.has_protectcooldown()) {
        settings.set_protectcooldown(50);
    }
    if (!settings.has_healcooldown()) {
        settings.set_healcooldown(20);
    }
    if (!settings.has_healperturn()) {
        settings.set_healperturn(2);
    }
    if (!settings.has_maxplayers()) {
        settings.set_maxplayers(4);
    }
}

boost::asio::awaitable<void> Playfield::reset() {
    robots.clear();
    const std::vector<std::vector<Vector2>> robotsPlacementMap = {
        {{0.5f, 0.5f}},
        {{0.0f, 0.0f}, {1.0f, 1.0f}},
        {{0.0f, 0.0f}, {0.5f, 0.5f}, {1.0f, 1.0f}},
        {{0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 1.0f}}
    };
    const auto& robotsPlacement = robotsPlacementMap.at(room.clients.size());
    for (unsigned client = 0; client != room.clients.size(); client++) {
        const auto genericTeamRobotsPlacement = robotsPlacement[client].toGeneric();
        for (unsigned robot = 0; robot != room.info.settings().robotsperclient(); robot++) {
            // Create robot
            Generic::Robot genericRobot;
            genericRobot.set_id(robot);
            genericRobot.set_client(client);
            genericRobot.set_state(Generic::Robot::Idle);
            genericRobot.set_health(20);
            *genericRobot.mutable_position() = genericTeamRobotsPlacement;
            // Add to list
            robots.push_back({genericRobot});
        }
    }
    // Broadcast new playfield
    auto playfieldSync = getPlayfieldSync();
    for (auto& client : room.clients) {
        Connection::Packet packet;
        packet.op.set_code(Generic::Operation::RobotUpdate);
        packet.data = std::make_unique<Generic::PlayfieldSync>(playfieldSync);
        co_await client->sendPacket(packet);
    }
}

boost::asio::awaitable<void> Playfield::updateRobot(const Connection::Client *sender, Generic::Robot& newRobot) {
    turn++;
    // Get local robot
    auto& localRobot = getLocalRobot(sender, newRobot.id());
    // Get distance travelled
    auto distanceTravelled = getDistanceBetween(*localRobot, newRobot);
    if (distanceTravelled != 0.0f) {
        // Check that robot is able to travel
        if (newRobot.state() != Generic::Robot::Idle) {
            throw Connection::Error("Can't travel right now", Generic::Error::IllegalOperation);
        }
        // Check that robot didn't travel too far
        if (distanceTravelled > room.info.settings().maxdistanceperturn()) {
            throw Connection::Error("Can't travel that far ("+std::to_string(distanceTravelled)+" > "+std::to_string(room.info.settings().maxdistanceperturn())+')', Generic::Error::IllegalOperation);
        }
    }
    // Check for illegal changes
    if (localRobot->health() != newRobot.health()) {
        throw Connection::Error("Can't change value", Generic::Error::IllegalOperation);
    }
    if (newRobot.position().x() > 1.0f || newRobot.position().x() < 0.0f || newRobot.position().y() > 1.0f || newRobot.position().y() < 0.0f) {
        throw Connection::Error("Position is out of range", Generic::Error::OutOfRange);
    }
    // Apply heal
    if (newRobot.state() == Generic::Robot::Heal) {
        newRobot.set_health(localRobot->health() + room.info.settings().healperturn());
        if (newRobot.health() > room.info.settings().maxrobothealth()) {
            newRobot.set_health(room.info.settings().maxrobothealth());
            newRobot.set_state(Generic::Robot::Idle);
        }
    }
    // Check for state change
    if (localRobot->state() != newRobot.state()) {
        // Enforce cooldowns
        if (newRobot.state() == Generic::Robot::Protect && turn - localRobot.stateTimer < room.info.settings().protectcooldown()
         || newRobot.state() == Generic::Robot::Heal && turn - localRobot.stateTimer < room.info.settings().healcooldown()) {
            throw Connection::Error("Can't change state yet (cooldown still in progress)", Generic::Error::IllegalOperation);
        }
        // Reset timer
        localRobot.stateTimer = turn;
    }
    // Apply changes to local robot
    *localRobot = newRobot;
    // Broadcast new robot
    for (auto& client : room.clients) {
        Connection::Packet packet;
        packet.op.set_code(Generic::Operation::RobotUpdate);
        packet.data = std::make_unique<Generic::Robot>(*localRobot);
        co_await client->sendPacket(packet);
    }
}

LocalRobot& Playfield::getLocalRobot(const Connection::Client *sender, unsigned robot) {
    // Check that new robot is in range
    if (robot >= robots.size()) {
        throw Connection::Error("Robot is out of range ("+std::to_string(robot)+" >= "+std::to_string(robots.size())+')', Generic::Error::OutOfRange);
    }
    // Get local robot
    auto& localRobot = robots[robot];
    // Check that robot belongs to sender
    if (room.clients[localRobot->client()].get() == sender) {
        throw Connection::Error("Robot does not belong to you", Generic::Error::IllegalOperation);
    }
    // Return local robot
    return localRobot;
}

Generic::RoomInfo Room::getFullRoomInfo() const {
    Generic::RoomInfo fres = info;
    for (const auto& client : clients) {
        *fres.add_members() = client->getUserInfo().publicinfo();
    }
    return fres;
}

boost::asio::awaitable<void> Room::addClient(const std::shared_ptr<Connection::Client>& client) {
    // Check that room is not full
    if (isFull()) {
        throw Connection::Error("Room is full", Generic::Error::RoomFull);
    }
    // Check that room has no game running
    if (isRunning()) {
        throw Connection::Error("Room is in game", Generic::Error::RoomNotAvailable);
    }
    // Broadcast join
    Connection::Packet packet;
    packet.op.set_code(Generic::Operation::JoinRoom);
    packet.data = std::make_unique<Generic::PublicUserInfo>(client->getUserInfo().publicinfo());
    for (auto& client : clients) {
        co_await client->sendPacket(packet);
    }
    // Add client
    clients.push_back(client);
    client->currentRoom = this;
    // Send room info to client
    packet.op.set_code(Generic::Operation::RoomInfoSync);
    packet.data = std::make_unique<Generic::RoomInfo>(getFullRoomInfo());
    co_await client->sendPacket(packet);
}

boost::asio::awaitable<void> Room::removeClient(const std::shared_ptr<Connection::Client>& client) {
    // Broadcast leave
    Connection::Packet packet;
    packet.op.set_code(Generic::Operation::LeaveRoom);
    packet.data = std::make_unique<Generic::UserReference>(client->getUserInfo().publicinfo().reference());
    for (auto& client : clients) {
        co_await client->sendPacket(packet);
    }
    // Remove client from list
    clients.erase(std::find(clients.begin(), clients.end(), client));
    client->currentRoom = nullptr;
    // Remove room if now empty
    if (clients.empty()) {
        removeRoom();
    }
}

void Room::removeRoom() {
    global.rooms.erase(std::find_if(global.rooms.begin(), global.rooms.end(), [this] (auto o) {return o.get() == this;}));
}

boost::asio::awaitable<void> Room::removeAllClients() {
    for (auto& client : clients) {
        co_await removeClient(client);
    }
}
}