#include <UserDB/UserDB.hpp>
#include <UserPermission/UserPermission.hpp>
#include <Serialize/Serialize.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <World/LocalWorld/ServerWorld/ServerWorld.hpp>
#include <ChunkWindow/ChunkWindow.hpp>
#include <ChatCommand/ApproveCommand/ApproveCommand.hpp>
#include <ChatCommand/KickCommand/KickCommand.hpp>
#include <ChatCommand/GrantCommand/GrantCommand.hpp>
#include <ChatCommand/RevokeCommand/RevokeCommand.hpp>
#include <ChatCommand/BanCommand/BanCommand.hpp>
#include <ChatCommand/UnbanCommand/UnbanCommand.hpp>
#include <Milestone/DiggingDeeperMilestone/DiggingDeeperMilestone.hpp>

ServerWorld::ServerWorld(std::string worldDir, ScheduleTaskCallback scheduleTask) :
    LocalWorld(worldDir),
    scheduleTask(scheduleTask)
{
    registerChatCommand<ApproveCommand>("approve");
    registerChatCommand<KickCommand>("kick", *this);
    registerChatCommand<GrantCommand>("grant", *this);
    registerChatCommand<RevokeCommand>("revoke", *this);
    registerChatCommand<BanCommand>("ban", *this);
    registerChatCommand<UnbanCommand>("unban", *this);
}

std::shared_ptr<Player> ServerWorld::addPlayer(std::string userName, UserDB::PermissionSet perms)
{
    if (m_players.count(userName) != 0)
    {
        return nullptr;
    }

    uint64_t id = allocEntityId();

    std::shared_ptr<Player> player = std::make_shared<Player>(*this, "main:player", id, userName);
    player->perms = perms;

    std::vector<uint8_t> bytes;
    if (planner.read(formatString("players/$+.dat", player->getName()), dataStore, bytes))
    {
        player->deserializeState(bytes.data(), bytes.size());
    }

    player->m_name = userName;
    
    sendChatToAllPlayers(formatString("'$+' joined the game.", userName), Color::success);

    entities[id] = player;
    m_players[userName] = player;
    return player;
}

void ServerWorld::removePlayer(std::string userName)
{
    std::shared_ptr<Player> player = m_players[userName];
    assert(player != nullptr);

    m_players.erase(m_players.find(userName));

    auto playerEntId = entities.find(player->id);
    if (playerEntId != entities.end())
    {
        entities.erase(playerEntId);
    }

    sendDeleteEntityMessageToClients(player->id);
    sendChatToAllPlayers(formatString("'$+' left the game.", userName), Color::success);
    
    if (player->client != nullptr)
    {
        player->client->disconnect();
    }
}

std::vector<std::string> ServerWorld::getPlayerNames()
{
    std::vector<std::string> names;

    for (const auto &[name, _] : m_players)
    {
        names.push_back(name);
    }

    return names;
}

std::vector<std::string> ServerWorld::getTypingPlayerNames(const std::string &skipPlayer)
{
    std::vector<std::string> names;

    for (const auto &[name, player] : m_players)
    {
        if (player->isTyping && name != skipPlayer)
        {
            names.push_back(name);
        }
    }

    return names;
}

void ServerWorld::serializeBeltViews(std::vector<uint8_t> &outBytes)
{
    std::shared_lock<std::shared_mutex> lock(beltViewMutex);
    serializeInto(outBytes, beltViews);
}

void ServerWorld::sendChatToAllPlayers(std::string text, glm::vec3 color)
{
    for (const auto& [userName, player] : m_players)
    {
        std::shared_ptr<Messenger> client = player->client;
        if (client != nullptr)
        {
            client->sendChat(text, color);
        }
    }
}

void ServerWorld::sendCreateEntityMessageToClients(std::shared_ptr<Entity> entity)
{
    for (const auto& [userName, player] : m_players)
    {
        std::shared_ptr<Messenger> client = player->client;
        if (client != nullptr)
        {
            client->sendCreateEntityMessage(entity);
        }
    }
}

void ServerWorld::sendDeleteEntityMessageToClients(uint64_t id)
{
    for (const auto& [userName, player] : m_players)
    {
        std::shared_ptr<Messenger> client = player->client;
        if (client != nullptr)
        {
            client->sendPlain(Protocol::MessageType::DeleteEntity, id);
        }
    }
}

void ServerWorld::onChunkLoaded(std::shared_ptr<Chunk> chunk)
{
    scheduleTask([this, chunk]()
    {
        if (chunk->purged)
        {
            return;
        }

        for (const auto &[_, player] : m_players)
        {
            if (player->client != nullptr)
            {
                player->client->sendChunk(*chunk);
            }
        }
    });
}

void ServerWorld::sendMessageToEntity(uint64_t id, const void *msg, size_t size)
{
    for (const auto& [userName, player] : m_players)
    {
        std::shared_ptr<Messenger> client = player->client;
        if (client != nullptr)
        {
            client->sendEntityMessage(id, msg, size);
        }
    }
}

void ServerWorld::fastUpdate(ChunkWindow *chunkWindow)
{
    // NOP
}

void ServerWorld::update()
{
    LocalWorld::update();

    // Send the block updates.
    if (m_players.size() != 0 && m_blockUpdates.size() != 0)
    {
        for (const auto& [userName, player] : m_players)
        {
            std::shared_ptr<Messenger> client = player->client;
            if (client != nullptr)
            {
                client->sendBlockUpdates(m_blockUpdates.data(), m_blockUpdates.size());
            }
        }

        m_blockUpdates.clear();
    }
}

void ServerWorld::onBlockGridUpdated(glm::ivec3 pos, int id, uint8_t meta)
{
    if (m_players.size() != 0)
    {
        m_blockUpdates.push_back(Protocol::BlockUpdate());
        Protocol::BlockUpdate &update = m_blockUpdates.back();

        memset(&update, 0, sizeof(Protocol::BlockUpdate));
        update.pos = pos;
        update.blockId = id;
        update.meta = meta;
    }
}

void ServerWorld::onPlayerUpdateBegins(Player &player)
{
    const BoundingBox<cint32> playerBox = player.getViewBox();
    if (player.client != nullptr)
    {
        player.client->setVisibleChunkBox(playerBox);
    }

    player.view.setRetainBox(playerBox, [&player](Chunk &chunk)
    {
        if (chunk.isReady() && player.client != nullptr)
        {
            player.client->sendChunk(chunk);
        }
    });

    // TODO: Figure out how to structure this so we don't have to duplicate from LocalWorld.
    player.updateDiggingState(*this);

    if (player.pos.z < wfixed(-128))
    {
        player.achieve(DiggingDeeperMilestone::ID, DiggingDeeperMilestone::BreakRingwooditeLayer);
    }
}

void ServerWorld::onPlayerUpdateOver(Player &player)
{
    // Save to the player file.
    player.serializeState(planner.newFile(formatString("players/$+.dat", player.getName())));

    // Send a state update to clients.
    Player::StateUpdateMessage msg;
    msg.type = Player::MessageType::StateUpdate;
    msg.pos = player.pos;
    msg.vel = player.vel;
    msg.theta = player.theta;
    msg.phi = player.phi;
    msg.controlFlags = player.controlFlags;
    msg.noclip = player.noclip;
    player.sendPlainMessage(msg);
}

void ServerWorld::onPlayerMessage(Player &player, const void *msg, size_t size)
{
    const Player::MessageHeader *header = reinterpret_cast<const Player::MessageHeader*>(msg);
    if (header->type == Player::MessageType::SendChat && size > 4)
    {
        const char *msgAsChars = reinterpret_cast<const char*>(msg);

        char *buffer = new char[size - 4 + 1];
        buffer[size - 4] = 0;
        std::memcpy(buffer, msgAsChars + 4, size - 4);

        std::string chat(buffer);
        delete[] buffer;

        if (chat.size() == 0)
        {
            return;
        }

        for (char& c : chat)
        {
            if (c == '\n')
            {
                c = ' ';
            }
        }

        if (chat[0] == '/')
        {
            processCommand(player, chat.substr(1));
        }
        else
        {
            Log::info(formatString("[CHAT] <$+> $+", player.getName(), chat));
            sendChatToAllPlayers(formatString("<$+> $+", player.getName(), chat));
        }
    }
    else if (header->type == Player::MessageType::StateUpdate && size == sizeof(Player::StateUpdateMessage))
    {
        const Player::StateUpdateMessage *updateMessage = reinterpret_cast<const Player::StateUpdateMessage*>(msg);
        if (player.controlFlags.contains(Player::ControlFlag::Resync))
        {
            // We are currently in resync, only accept responses.
            if (updateMessage->controlFlags.contains(Player::ControlFlag::Resync))
            {
                player.controlFlags -= Player::ControlFlag::Resync;
            }
            else
            {
                return;
            }
        }

        player.controlFlags = updateMessage->controlFlags;
        player.controlFlags -= Player::ControlFlag::Resync;

        if (player.canGoTo(updateMessage->pos) == true)
        {
            player.pos = updateMessage->pos;
            player.vel = updateMessage->vel;
            player.theta = updateMessage->theta;
            player.phi = updateMessage->phi;
        }
        else
        {
            // Force a resync.
            player.controlFlags += Player::ControlFlag::Resync;

            Player::StateUpdateMessage msg;
            msg.type = Player::MessageType::StateUpdate;
            msg.pos = player.pos;
            msg.vel = player.vel;
            msg.theta = player.theta;
            msg.phi = player.phi;
            msg.controlFlags = player.controlFlags;
            msg.noclip = player.noclip;
            player.sendPlainMessage(msg);
        }
    }
    else if (header->type == Player::MessageType::SetTypingStatus)
    {
        bool currentlyTyping;
        deserialize((const uint8_t*) msg + sizeof(Player::MessageType), size - sizeof(Player::MessageType), currentlyTyping);

        player.isTyping = currentlyTyping;
        player.sendSerialMessage(Player::MessageType::SetTypingStatus, currentlyTyping);
    }
    else
    {
        LocalWorld::onPlayerMessage(player, msg, size);
    }
}

void ServerWorld::playSound(Sound &sound, glm::ivec3 blockPos, glm::vec3 vel)
{
    for (const auto& [userName, player] : m_players)
    {
        player->client->sendPlaySoundMessage(sound.name, blockPos, vel);
    }
}

void ServerWorld::onBeltViewUpdated(BeltView &view)
{
    for (const auto& [userName, player] : m_players)
    {
        player->client->sendBeltViewUpdatedMessage(view);
    }
}

void ServerWorld::onBeltViewRemoved(uint64_t id)
{
    for (const auto& [userName, player] : m_players)
    {
        player->client->sendBeltViewRemovedMessage(id);
    }
}

void ServerWorld::close()
{
    for (const auto& [userName, player] : m_players)
    {
        player->isBeingForcedOut = true;
        player->client->sendKickedFromServerMessage("The server is shutting down.");
    }

    LocalWorld::close();
}

bool ServerWorld::isShutdownReady()
{
    return m_players.size() == 0;
}

std::shared_ptr<Player> ServerWorld::getPlayer(const std::string &userName)
{
    auto it = m_players.find(userName);
    if (it == m_players.end())
    {
        return nullptr;
    }

    return it->second;
}