#include <Serialize/Base.hpp>
#include <Serialize/Serialize.hpp>
#include <Utilities/CircularInt.hpp>
#include <Utilities/Extern.hpp>
#include <Widget/Builder/Builder.hpp>
#include <Widget/Label/Label.hpp>
#include <World/RemoteWorld/RemoteWorld.hpp>
#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <Settings/Settings.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Utilities/Utilities.hpp>
#include <Utilities/Socket.hpp>
#include <Protocol/Protocol.hpp>
#include <Log/Log.hpp>
#include <EntityRegistry/EntityRegistry.hpp>
#include <World/World.hpp>
#include <UserDB/UserDB.hpp>
#include <Utilities/ServerSessionTable.hpp>
#include <ChunkWindow/ChunkWindow.hpp>

void RemoteWorld::reportFatalError(std::string error)
{
    // Log the error and then report as fatal.
    Log::error(error);
    fatalError = error;
}

void RemoteWorld::onMessage(Protocol::MessageType type, void *payload, uint32_t payloadSize)
{
    if (type == Protocol::MessageType::KeepAlive)
    {
        uint32_t timeOfDay32;
        deserialize((uint8_t*) payload, payloadSize, timeOfDay32, playerNames, typingPlayerNames, syncWorldTime);
        syncTicks = SDL_GetTicks();
        
        int timeOfDay = (int) timeOfDay32;
        if (std::abs(timeOfDay - this->timeOfDay) > 15)
        {
            setTimeOfDay(timeOfDay);
        }

        pingTicks = SDL_GetTicks() - lastKeepAliveTicks;
        typingPlayerUpdateCount++;
    }
    else if (type == Protocol::MessageType::ProtocolError)
    {
        std::string message((char*) payload, payloadSize);
        reportFatalError(formatString("Protocol error:\n$+", message));
    }
    else if (type == Protocol::MessageType::SetVisibleChunkBox)
    {
        BoundingBox<cint32> newBox;
        deserialize((const uint8_t*) payload, payloadSize, newBox);
        visibleChunkBox = newBox;
        chunkWindow->setVisibleBox(newBox);
    }
    else if (type == Protocol::MessageType::ChunkData)
    {
        if (payloadSize < 4)
        {
            reportFatalError(formatString("Received a ChunkData message with insufficient payload."));
            return;
        }

        const uint8_t *header = (const uint8_t*) payload;
        wvec3 chunkBase(visibleChunkBox.x0, visibleChunkBox.y0, visibleChunkBox.z0);
        chunkBase.x += header[0] << 4;
        chunkBase.y += header[1] << 4;
        chunkBase.z += header[2] << 4;

        auto chunk = getChunk(glm::ivec3(chunkBase));
        if (chunk != nullptr)
        {
            chunk->fromNetworkPayload(header + 3, payloadSize - 3);
        }
    }
    else if (type == Protocol::MessageType::BlockUpdateList)
    {
        size_t numUpdates = payloadSize / sizeof(Protocol::BlockUpdate);
        Protocol::BlockUpdate *update = reinterpret_cast<Protocol::BlockUpdate*>(payload);

        while (numUpdates--)
        {
            glm::ivec3 chunkBase = update->pos & glm::ivec3(~0xF);
            glm::ivec3 chunkOffset = update->pos & glm::ivec3(0xF);

            std::shared_ptr<Chunk> chunk = getChunk(chunkBase);
            if (chunk == nullptr || !chunk->isReady())
            {
                return;
            }

            bool hadEffectsBefore = chunk->hasBlockEffects();

            chunk->setBlockAndMeta(chunkOffset, update->blockId, update->meta);
            chunk->onBlockUpdated(chunkOffset);

            if (window != nullptr && chunk->hasBlockEffects() && !hadEffectsBefore)
            {
                window->addChunkWithEffects(chunk);
            }
            
            update++;
        }
    }
    else if (type == Protocol::MessageType::EntityMessage)
    {
        if (payloadSize < sizeof(Protocol::EntityMessage))
        {
            Log::error("EntityMessage too short");
        }
        else
        {
            Protocol::EntityMessage *entityMessage = reinterpret_cast<Protocol::EntityMessage*>(payload);
            if (entities.count(entityMessage->entityID) == 1)
            {
                entities[entityMessage->entityID]->onMessage(
                    entityMessage->payload, payloadSize - sizeof(Protocol::EntityMessage)
                );
            }
        }
    }
    else if (type == Protocol::MessageType::CreateEntity && payloadSize != 0)
    {
        uint64_t id;
        std::string typeId;
        NestedPayload nested;

        if (deserialize((uint8_t*)payload, payloadSize, id, typeId, nested) != payloadSize)
        {
            reportFatalError("Failed to deserialize CreateEntity message!");
        }
        else
        {
            std::shared_ptr<Entity> entity = entityRegistry.createEntity(*this, typeId, id, nested.buffer.data(), nested.buffer.size());
            if (entity == nullptr)
            {
                reportFatalError(formatString("Failed to create entity of type '$+'", typeId));
            }
            else
            {
                entities[entity->id] = entity;
            }
        }
    }
    else if (type == Protocol::MessageType::DeleteEntity && payloadSize == sizeof(Protocol::DeleteEntityMessage))
    {
        Protocol::DeleteEntityMessage *msg = reinterpret_cast<Protocol::DeleteEntityMessage*>(payload);
        if (entities.count(msg->id) == 1)
        {
            entities.erase(entities.find(msg->id));
        }
    }
    else if (type == Protocol::MessageType::Chat && payloadSize > 3)
    {
        uint8_t *data = reinterpret_cast<uint8_t*>(payload);
        glm::vec3 color;

        int i;
        for (i=0; i<3; i++)
        {
            color[i] = (float) data[i] / 255.0f;
        }

        std::string text((char*) (data + 3), (char*) (data + payloadSize));
        ourPlayer->writeChat(text, color);
    }
    else if (type == Protocol::MessageType::PlaySound)
    {
        std::string soundName;
        glm::ivec3 blockPos;
        glm::vec3 vel;

        deserialize(reinterpret_cast<uint8_t*>(payload), payloadSize, soundName, blockPos, vel);

        Sound::byName(soundName).play(glm::vec3(blockPos) + glm::vec3(0.5f), vel);
    }
    else if (type == Protocol::MessageType::BeltViewUpdated)
    {
        BeltView view;
        deserialize(reinterpret_cast<uint8_t*>(payload), payloadSize, view.state.id, view);

        std::unique_lock<std::shared_mutex> lock(beltViewMutex);
        beltViews[view.state.id] = view;
        assignBeltViewSprite(view.state.id);
    }
    else if (type == Protocol::MessageType::BeltViewRemoved)
    {
        uint64_t id;
        deserialize(reinterpret_cast<uint8_t*>(payload), payloadSize, id);
        
        std::unique_lock<std::shared_mutex> lock(beltViewMutex);
        beltViews.erase(id);
        eraseBeltViewSprite(id);
    }
    else if (type == Protocol::MessageType::KickedFromServer)
    {
        std::string reason(reinterpret_cast<char*>(payload), payloadSize);

        reportFatalError(formatString("You've been kicked from the server:\n$+", reason));
    }
    else if (type == Protocol::MessageType::AttachController)
    {
        uint8_t channelId;
        uint16_t stamp;
        std::string controllerId;
        deserialize(static_cast<uint8_t*>(payload), payloadSize, channelId, stamp, controllerId);

        auto *channel = ourPlayer->getControllerChannel(channelId);
        if (channel == nullptr)
        {
            reportFatalError(formatString("AttachController: Invalid controller channel ID: $+", (int) channelId));
            return;
        }

        channel->attach(controllerId, stamp);
    }
    else if (type == Protocol::MessageType::DetachController)
    {
        uint8_t channelId;

        deserialize(static_cast<uint8_t*>(payload), payloadSize, channelId);

        auto *channel = ourPlayer->getControllerChannel(channelId);
        if (channel == nullptr)
        {
            reportFatalError(formatString("DetachController: Invalid controller channel ID: $+", (int) channelId));
            return;
        }

        channel->detach();
    }
    else if (type == Protocol::MessageType::RemoteCall)
    {
        uint8_t channelId;
        uint8_t opcode;
        uint16_t stamp;
        uint64_t promiseId;
        NestedPayload rpcPayload;

        if (deserialize(
            static_cast<uint8_t*>(payload), payloadSize,
            channelId,
            opcode,
            stamp,
            promiseId,
            rpcPayload
        ) != payloadSize)
        {
            reportFatalError("Received a malformed RemoteCall message.");
            return;
        }

        auto *channel = ourPlayer->getControllerChannel(channelId);
        if (channel == nullptr)
        {
            reportFatalError(formatString("RemoteCall: Invalid controller channel ID: $+", (int) channelId));
            return;
        }

        channel->dispatch(opcode, stamp, promiseId, rpcPayload);
    }
    else if (type == Protocol::MessageType::RemoteReturn)
    {
        uint8_t channelId;
        uint16_t stamp;
        uint64_t promiseId;
        NestedPayload rpcPayload;

        if (deserialize(
            static_cast<uint8_t*>(payload), payloadSize,
            channelId,
            stamp,
            promiseId,
            rpcPayload
        ) != payloadSize)
        {
            reportFatalError("Received a malformed RemoteReturn message.");
            return;
        }

        auto *channel = ourPlayer->getControllerChannel(channelId);
        if (channel == nullptr)
        {
            reportFatalError(formatString("RemoteReturn: Invalid controller channel ID: $+", (int) channelId));
            return;
        }

        channel->resolve(stamp, promiseId, rpcPayload);
    }
    else
    {
        reportFatalError(formatString("Received unsupported message type: $+", static_cast<uint32_t>(type)));
    }
}

RemoteWorld::RemoteWorld(const ConnectionData &data) : lastKeepAliveTicks(SDL_GetTicks())
{
    socket = data.socket;
    messenger = std::make_shared<Messenger>(data.socket);
    weather.init(data.weatherNode);

    syncWorldTime = data.worldTime;
    syncTicks = SDL_GetTicks();

    // Create the player.
    std::shared_ptr<Player> player = std::make_shared<Player>(*this, "main:player", data.playerEntityID, data.userName);
    if (player->deserializeState((uint8_t*) data.playerData, data.playerDataSize) != data.playerDataSize)
    {
        fatalError = "Failed to deserialize the player data!";
    }

    // Delete the player data.
    delete[] data.playerData;

    // Register the player entity.
    entities[data.playerEntityID] = player;
    
    // Initialize the world.
    ourPlayer = player;
    setTimeOfDay(data.timeOfDay);
    onBlockTableLoaded(data.blockTable);
    deserialize(data.beltData.data(), data.beltData.size(), beltViews);

    // Create the receiving thread.
    std::thread([this]()
    {
        messenger->processIncoming(
            [this](Protocol::MessageType type, const std::vector<uint8_t> &messagePayload)
            {
                if (isRequestingDisconnect)
                {
                    return false;
                }
    
                std::vector<uint8_t> payload = messagePayload;
                this->commandQueue.add([type, payload](RemoteWorld &world)
                {
                    world.onMessage(type, (void*) payload.data(), payload.size());
                });

                return true;
            }
        );

        // Notify of disconnection by setting the fatal error string.
        this->commandQueue.add([](RemoteWorld &world)
        {
            if (world.fatalError == "")
            {
                world.fatalError = "Server disconnected or timed out.";
            }
        });

        receivingThreadEnded = true;
    }).detach();
}

RemoteWorld::~RemoteWorld()
{
    messenger->disconnect();
}

std::shared_ptr<Player> RemoteWorld::getOurPlayer()
{
    return ourPlayer;
}

void RemoteWorld::sendMessageToEntity(uint64_t id, const void *msg, size_t size)
{
    messenger->sendEntityMessage(id, msg, size);
}

void RemoteWorld::sendCreateEntityMessageToClients(std::shared_ptr<Entity> entity)
{
    // NOP
}

void RemoteWorld::sendDeleteEntityMessageToClients(uint64_t id)
{
    // NOP
}

void RemoteWorld::onPlayerUpdateBegins(Player &player)
{
    if (ourPlayer.get() == &player)
    {
        if (
            player.clientPrefs.horizontalViewDistance != settings.horizontalViewDistance ||
            player.clientPrefs.verticalViewDistance != settings.verticalViewDistance
        )
        {
            player.clientPrefs.horizontalViewDistance = settings.horizontalViewDistance;
            player.clientPrefs.verticalViewDistance = settings.verticalViewDistance;
            
            messenger->sendSetClientPrefs(player.clientPrefs);
        }

        bool currentlyTyping = lastTypingTicks != 0 && SDL_GetTicks() - lastTypingTicks < Player::typingTimeoutTicks;
        if (currentlyTyping != player.isTyping)
        {
            player.isTyping = currentlyTyping;
            player.sendSerialMessage(Player::MessageType::SetTypingStatus, currentlyTyping);
        }
    }
}

void RemoteWorld::onPlayerUpdateOver(Player &player)
{
    if (ourPlayer.get() == &player)
    {
        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 RemoteWorld::drawPlayerModel(GraphicsContext &context, WorldDisplay &worldDisplay, Player &player)
{
    if (ourPlayer.get() != &player)
    {
        player.drawModel(context, worldDisplay, *ourPlayer);
    }
}

void RemoteWorld::onPlayerMessage(Player &player, const void *msg, size_t size)
{
    const Player::MessageHeader *header = reinterpret_cast<const Player::MessageHeader*>(msg);
    if (header->type == Player::MessageType::StateUpdate && size == sizeof(Player::StateUpdateMessage))
    {
        const Player::StateUpdateMessage *updateMessage = reinterpret_cast<const Player::StateUpdateMessage*>(msg);
        if (player.canGoTo(player.pos) != true)
        {
            // If we are inside a block, it means we are definitely out-of-sync
            // and need to be updated.
            player.vel = updateMessage->vel;
            player.pos = updateMessage->pos;
            player.noclip = updateMessage->noclip;
        }
        else if (ourPlayer.get() == &player)
        {
            // This is our player, only update if resync is demanded.
            player.noclip = updateMessage->noclip;
            if (updateMessage->controlFlags.contains(Player::ControlFlag::Resync))
            {
                player.pos = updateMessage->pos;
                player.vel = updateMessage->vel;
                player.theta = updateMessage->theta;
                player.phi = updateMessage->phi;

                // Send a resync response back to the server.
                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.controlFlags += Player::ControlFlag::Resync;
                msg.noclip = player.noclip;
                player.sendPlainMessage(msg);
            }
        }
        else
        {
            // Another player, use heuristics to figure out whether we should update.
            player.controlFlags = updateMessage->controlFlags;
            player.noclip = updateMessage->noclip;

            if (glm::length(glm::vec3(updateMessage->vel - player.vel)) > 1.0f)
            {
                player.vel = updateMessage->vel;
            }

            if (glm::length(glm::vec3(updateMessage->pos - player.pos)) > glm::length(glm::vec3(player.vel))*5.0f)
            {
                player.pos = updateMessage->pos;
            }

            player.theta = updateMessage->theta;
            player.phi = updateMessage->phi;
        }
    }
    else if (header->type == Player::MessageType::UpdateMapPins)
    {
        Player::MessageType msgType;
        deserialize((const uint8_t*) msg, size, msgType, player.mapPins);
    }
    else if (header->type == Player::MessageType::UpdateInventorySlot)
    {
        uint32_t msgType = 0;
        std::string invId;
        uint32_t slotIndex = 0;
        ItemStack stack;

        if (deserialize((const uint8_t*) msg, size, msgType, invId, slotIndex, stack) == size)
        {
            player.items.store(invId).setStackInSlot(slotIndex, stack);
        }
    }
    else if (header->type == Player::MessageType::SetTypingStatus)
    {
        if (&player == ourPlayer.get())
        {
            return;
        }

        bool currentlyTyping;
        deserialize((const uint8_t*) msg + sizeof(Player::MessageType), size - sizeof(Player::MessageType), currentlyTyping);
        
        if (currentlyTyping)
        {
            typingPlayerNames.insert(player.getName());
        }
        else
        {
            typingPlayerNames.erase(player.getName());
        }

        typingPlayerUpdateCount++;
    }
}

void RemoteWorld::onChunkRequested(std::shared_ptr<Chunk> chunk, float relativeDeadline)
{
    requestedChunks.insert(chunk);
}

void RemoteWorld::close()
{
    isRequestingDisconnect = true;
    messenger->sendEmpty(Protocol::MessageType::Logout);
    entities.clear();
    ourPlayer = nullptr;

    for (auto &chunk : requestedChunks)
    {
        chunk->markLoadingComplete();
    }
}

void RemoteWorld::fastUpdate(ChunkWindow *chunkWindow)
{
    this->chunkWindow = chunkWindow;

    // Process all commands.
    commandQueue.processAllCommands(*this);
}

void RemoteWorld::update()
{
    // Update time-of-day.
    timeOfDay = (timeOfDay + 1) % World::cycleDuration;

    // Check if we need to send a KeepAlive message.
    if (SDL_GetTicks()-lastKeepAliveTicks > 1500)
    {
        lastKeepAliveTicks = SDL_GetTicks();
        messenger->sendClientKeepAlive(lastTypingTicks != 0 && SDL_GetTicks() - lastTypingTicks < Player::typingTimeoutTicks);
    }

    // Update entities.
    for (const auto& [id, entity] : entities)
    {
        entity->update();
    };

    advanceBeltViews();

    // Cache chunks.
    housekeep();
}

bool RemoteWorld::isLocal()
{
    return false;
}

void RemoteWorld::drawBlockSelector(GraphicsContext &context, WorldDisplay &worldDisplay, Player &player)
{
    if (ourPlayer.get() == &player)
    {
        player.drawBlockSelector(context, worldDisplay);
    }
}

bool RemoteWorld::canPurgeChunk(Chunk &chunk)
{
    return true;
}

void RemoteWorld::onWorldGridNodeCreated(std::shared_ptr<WorldGridNode> node)
{
    node->markLoaded();
}

void RemoteWorld::playSound(Sound &sound, glm::ivec3 blockPos, glm::vec3 vel)
{
    crash("RemoteWorld::playSound() called (this is an invalid opeartion)");
}

void RemoteWorld::writeDebugStats(std::ostream &os)
{
    os << "Ping: " << pingTicks << "ms" << std::endl;
}

bool RemoteWorld::isShutdownReady()
{
    return receivingThreadEnded;
}

uint64_t RemoteWorld::getWorldTime()
{
    uint64_t updatesPassed = (SDL_GetTicks() - syncTicks) / updateDuration;
    return syncWorldTime + updatesPassed;
}

void RemoteWorld::getWorldStats(StatList &statList)
{
    statList.push_back({
        "Total world time:",
        new Builder<uint64_t>(
            [this]() { return getWorldTime(); },
            [](uint64_t ticks) -> Widget*
            {
                return new Label(formatTicks(ticks));
            }
        ),
    });
}

std::shared_ptr<Messenger> RemoteWorld::getPeerMessenger(Player &player)
{
    return messenger;
}