#include <Controller/GadgetController/GadgetController.hpp>
#include <Controller/MapController/MapController.hpp>
#include <ControllerChannel/ControllerChannel.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Item/IEquipment.hpp>
#include <ItemStack/ItemStack.hpp>
#include <MachineComponent/AttributionComponent/AttributionComponent.hpp>
#include <MilestoneRegistry/MilestoneRegistry.hpp>
#include <PlayerTriggerRegistry/PlayerTriggerRegistry.hpp>
#include <Protocol/Protocol.hpp>
#include <Utilities/FixedPoint.hpp>
#include <Utilities/Parse.hpp>
#include <Serialize/Serialize.hpp>
#include <World/LocalWorld/ServerWorld/ServerWorld.hpp>
#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <Entity/PhysicsEntity/CameraEntity/CameraEntity.hpp>
#include <World/World.hpp>
#include <Utilities/Utilities.hpp>
#include <Log/Log.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <Settings/Settings.hpp>
#include <ControllerRegistry/ControllerRegistry.hpp>
#include <Controller/InventoryController/InventoryController.hpp>
#include <Entity/PhysicsEntity/ItemEntity/ItemEntity.hpp>
#include <ItemRegistry/ItemRegistry.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <MovementPhysics/MovementPhysics.hpp>
#include <World/RemoteWorld/RemoteWorld.hpp>
#include <UserDB/UserDB.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <VoxelSpriteRenderer/VoxelSpriteRenderer.hpp>
#include <Vk/ShaderConst.hpp>
#include <random>
#include <algorithm>

Player::HandTool Player::handToolInstance;

bool Player::HandTool::canDig(ItemStack &stack, Block &block)
{
    return block.canManuallyDig();
}

float Player::HandTool::getPowerAgainst(ItemStack &stack, Block &block)
{
    return 1.0f;
}

void Player::HandTool::harvest(ItemStack &stack, LocalWorld &world, glm::ivec3 pos)
{
    world.breakBlock(pos);
}

Sound& Player::HandTool::getDigSoundFor(ItemStack &stack, Block &block)
{
    return Sound::NONE;
}

Model& Player::getCracks(int stage)
{
    switch (stage)
    {
    case 1: return *Model::cracks1;
    case 2: return *Model::cracks2;
    case 3: return *Model::cracks3;
    case 4: return *Model::cracks4;
    case 5: return *Model::cracks5;
    case 6: return *Model::cracks6;
    default: return *Model::cracks7;
    }
}

void Player::sendSlotUpdate(const std::string &invId, int slot, ItemStack &stack)
{
    uint32_t msgType = (uint32_t) MessageType::UpdateInventorySlot;
    uint32_t slotIndex = slot;

    std::vector<uint8_t> msg = serialize(msgType, invId, slotIndex, stack);

    sendMessage(msg.data(), msg.size());
}

void Player::sendDigUpdate()
{
    UpdateDigStateMessage msg;
    msg.type = MessageType::UpdateDigState;
    msg.digState = m_digState;
    sendPlainMessage(msg);
}

void Player::updateCrouching()
{
    if (controlFlags.contains(ControlFlag::Crouch))
    {
        crouching = true;
    }
    else if (crouching)
    {
        crouching = false;
        
        if (canGoTo(pos) != true)
        {
            crouching = true;
        }
    }
}

Player::Player(World &world, std::string typeId, uint64_t id, std::string name) :
    CameraEntity(world, typeId, id),
    m_name(name),
    m_frameNum(1),
    guiChannel(*this, 0),
    gadgetChannels{
        ControllerChannel(*this, 1),
        ControllerChannel(*this, 2),
        ControllerChannel(*this, 3),
        ControllerChannel(*this, 4),
        ControllerChannel(*this, 5),
        ControllerChannel(*this, 6),
        ControllerChannel(*this, 7),
    },
    client(nullptr),
    view(world)
{
    items
        .withStore("inventory", numInventorySlots)
        .withStore(
            "equipment",
            numEquipmentSlots,
            ItemStore::Rules::empty() || ItemStore::Rules::itemExtends<IEquipment>()
        )
        .withStore("trash", 1);
    
    selectedSlot = items.store("inventory").begin();
    
    bool isServer = dynamic_cast<ServerWorld*>(&world) != nullptr;
    if (world.isLocal())
    {
        items.withStackUpdatedCallback([this, isServer](const std::string &invId, int slot, ItemStack &newStack)
        {
            if (isServer)
            {
                sendSlotUpdate(invId, slot, newStack);
            }
        });

        items.store("equipment").withStackUpdatedCallback([this](int slot, ItemStack &newStack)
        {
            reattachGadget(slot);

            if (newStack.isEmpty())
            {
                return;
            }

            newStack.getItem()->onEquippedBy(*this, newStack);
        });
    }
}

Player::~Player()
{
    selectedSlot = items.store("inventory").begin();
}

void Player::update()
{
    // Update the controllers.
    for (const auto &channel : ctrlChannelTable)
    {
        channel->update();
    }

    updateCrouching();

    // Tell the world that the player update is beginning.
    world->onPlayerUpdateBegins(*this);

    // Allow equipments to update.
    for (auto &stack : items.store("equipment"))
    {
        IEquipment *equipment = stack.getItemAs<IEquipment>();

        if (equipment == nullptr)
        {
            continue;
        }

        equipment->onPlayerUpdating(*this, stack);
    }

    wrapCamera();

    if (areChunksLoaded())
    {
        if (!noclip)
        {
            world
                ->getBlockAt(glm::floor(pos))
                ->getMovementPhysicsInside()
                .updatePlayer(*this);
            CameraEntity::update();
        }
        else
        {
            updateNoclip();
        }
    }
    
    // Do whatever needs to be done after update.
    world->onPlayerUpdateOver(*this);

    selectedSlot.flush();
}

wfvec3 Player::getEyePos()
{
    wfvec3 eyePos = pos;
    eyePos.z += wfixed(crouching ? crouchingHeadHeight : headHeight);
    eyePos.z -= wfixed(eyeFromHeadOffset);

    return eyePos;
}

void Player::wrapCamera()
{
    while (theta < 0.0f)
    {
        theta += fullCircle;
    }

    while (theta >= fullCircle)
    {
        theta -= fullCircle;
    }
}

float Player::getThirdPersonViewDistance()
{
    World::BlockHit hit = world->castRay(getEyePos(), getForwardVector(), 4.0f);
    if (!hit.hasHit)
    {
        return 4.0f;
    }
    
    return glm::length(glm::vec3(hit.collisionPoint - getEyePos())) - 0.1;
}

glm::vec3 Player::getForwardVector()
{
    return glm::inverse(getCameraTransform()) * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f);
}

glm::vec3 Player::getUpVector()
{
    return glm::inverse(getCameraTransform()) * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f);
}

glm::vec3 Player::getRightVector()
{
    return glm::inverse(getCameraTransform()) * glm::vec4(1.0f, 0.0f, 0.0f, 0.0f);
}

BoundingBox<wfixed> Player::getBoundingBox(wfvec3 pos)
{
    BoundingBox<wfixed> box;
    box.x0 = pos.x - wfixed(bodyRadius);
    box.x1 = pos.x + wfixed(bodyRadius);
    box.y0 = pos.y - wfixed(bodyRadius);
    box.y1 = pos.y + wfixed(bodyRadius);
    box.z0 = pos.z;
    box.z1 = pos.z + wfixed(headHeight);

    if (crouching)
    {
        box.z1 = pos.z + wfixed(crouchingHeadHeight);
    }

    return box;
}

glm::vec3 Player::getFrictionCoefficients()
{
    return glm::vec3(30.0f, 30.0f, 1.0f);
}

std::optional<bool> Player::canGoTo(wfvec3 pos)
{
    std::optional<bool> parentResult = CameraEntity::canGoTo(pos);
    if (parentResult != true)
    {
        return parentResult;
    }

    // If holding crouch input, don't fall off ledges.
    if (isOnGround && controlFlags.contains(Player::ControlFlag::Crouch))
    {
        // If the player would fall at the new position, don't move.
        if (CameraEntity::canGoTo(pos + wfvec3(0.0, 0.0, (-1.0/16.0))) == true)
        {
            return false;
        }
    }

    return true;
}

bool Player::onCollision()
{
    // Check if we can go upstairs.
    wfvec3 forwardOnlyDest = pos + vel;
    forwardOnlyDest.z = pos.z;

    if (canGoTo(forwardOnlyDest) == false)
    {
        wfvec3 upstairsDest = forwardOnlyDest;
        for (size_t i = 0; i < 8; i++)
        {
            upstairsDest += wfvec3(0.0, 0.0, wfixed(1.0) >> 3);
            if (canGoTo(upstairsDest) == true)
            {
                pos = upstairsDest;
                return true;
            }
        }
    }
    
    return false;
}

bool Player::areChunksLoaded()
{
    // Get our chunk coordinates.
    wvec3 playerChunkPos = wvec3(pos) & wvec3(~0xF);

    // return if chunks around the player aren't loaded yet.
    wvec3 delta = wvec3(0);
    for (delta.x=-cint(16); delta.x<=cint(16); delta.x+=cint(16))
    {
        for (delta.y=-cint(16); delta.y<=cint(16); delta.y+=cint(16))
        {
            for (delta.z=-cint(16); delta.z<=cint(16); delta.z+=cint(16))
            {
                std::shared_ptr<Chunk> playerChunk = world->getChunk(playerChunkPos + delta);
                if (playerChunk == nullptr || !playerChunk->isReady())
                {
                    return false;
                }
            }
        }
    }

    return true;
}

void Player::onRightClick()
{
    RightClickMessage msg;
    msg.type = MessageType::RightClick;
    msg.pos = getEyePos();
    msg.dir = getForwardVector();
    sendPlainMessage(msg);
}

void Player::onJumping()
{
    if (isOnGround)
    {
        isOnGround = false;
        this->vel[2] = wfixed(jumpVel);
    }
}

void Player::onSetHUD(int num)
{
    if (num == 0)
    {
        num = 10;
    }

    selectedSlot = items.store("inventory").at(num - 1);
    m_digState.isDigging = false;

    SetSelectedSlotMessage msg;
    msg.type = MessageType::SetSelectedSlot;
    msg.slot = selectedSlot.slotIndex();
    sendPlainMessage(msg);
}

void Player::onOpenInventory()
{
    MessageType msg = MessageType::OpenInventory;
    sendPlainMessage(msg);
}

void Player::onDrop()
{
    MessageType msg = MessageType::Drop;
    sendPlainMessage(msg);
}

void Player::onOpenMap()
{
    MessageType msg = MessageType::OpenMap;
    sendPlainMessage(msg);
}

void Player::updateNoclip()
{
    // In noclip mode ignore all physics.
    glm::vec3 direction = glm::vec3(sin(theta), cos(theta), 0.0f);
    glm::vec3 perpendicular = glm::vec3(sin(theta + 3.1415f * 0.5f), cos(theta + 3.1415f * 0.5f), 0.0f);
    if (controlFlags.contains(Player::ControlFlag::Forward))
    {
        pos += noclipVel * direction;
    }

    if (controlFlags.contains(Player::ControlFlag::Back))
    {
        pos -= noclipVel * direction;
    }

    if (controlFlags.contains(Player::ControlFlag::Right))
    {
        pos += noclipVel * perpendicular;
    }

    if (controlFlags.contains(Player::ControlFlag::Left))
    {
        pos -= noclipVel * perpendicular;
    }

    if (controlFlags.contains(Player::ControlFlag::Jump))
    {
        pos.z += wfixed(noclipVel);
    }

    if (controlFlags.contains(Player::ControlFlag::Crouch))
    {
        pos.z -= wfixed(noclipVel);
    }
}

bool Player::hasPermission(UserPermission perm)
{
    return perms.contains(perm);
}

void Player::teleport(glm::vec3 dest)
{
    pos = dest;

    // Force a resync.
    controlFlags += ControlFlag::Resync;
    
    StateUpdateMessage msg;
    msg.type = MessageType::StateUpdate;
    msg.pos = pos;
    msg.vel = vel;
    msg.theta = theta;
    msg.phi = phi;
    msg.controlFlags = controlFlags;
    msg.noclip = noclip;
    sendPlainMessage(msg);
}

void Player::setNoclip(bool noclip)
{
    this->noclip = noclip;

    StateUpdateMessage msg;
    msg.type = MessageType::StateUpdate;
    msg.pos = pos;
    msg.vel = vel;
    msg.theta = theta;
    msg.phi = phi;
    msg.controlFlags = controlFlags;
    msg.noclip = noclip;
    sendPlainMessage(msg);
}

bool Player::hasCompletedAchievement(const std::string &milestoneId, int achievementId)
{
    return milestoneBitmaps[milestoneId] & (static_cast<uint64_t>(1) << achievementId);
}

bool Player::isAchievementReachable(const std::string &milestoneId, int achievementId)
{
    Milestone &milestone = milestoneRegistry.getMilestone(milestoneId);
    auto &achievement = milestone.achievements[achievementId];

    for (const Milestone::Dependency &dep : achievement->deps)
    {
        if (!hasCompletedAchievement(dep.milestoneId, dep.achievementId))
        {
            return false;
        }
    }

    return true;
}

bool Player::hasCompletedMilestone(const std::string &milestoneId)
{
    Milestone &milestone = milestoneRegistry.getMilestone(milestoneId);
    uint64_t bitmap = milestoneBitmaps[milestoneId];

    return (bitmap & milestone.allAchievementBits) == milestone.allAchievementBits;
}

bool Player::isMilestoneReachable(const std::string &milestoneId)
{
    Milestone &milestone = milestoneRegistry.getMilestone(milestoneId);

    for (const auto &[achievementId, _] : milestone.achievements)
    {
        if (isAchievementReachable(milestoneId, achievementId))
        {
            return true;
        }
    }

    return false;
}

void Player::achieve(const std::string &milestoneId, int achievementId)
{
    if (hasCompletedAchievement(milestoneId, achievementId))
    {
        return;
    }

    milestoneBitmaps[milestoneId] |= static_cast<uint64_t>(1) << achievementId;

    Milestone &milestone = milestoneRegistry.getMilestone(milestoneId);
    auto &achievement = milestone.achievements[achievementId];

    ServerWorld *serverWorld = dynamic_cast<ServerWorld*>(world);
    if (serverWorld != nullptr)
    {
        serverWorld->sendChatToAllPlayers(
            formatString(
                "$+ACHIEVEMENT UNLOCKED by $+: $+$+",
                GraphicsContext::colorstr(Color::positiveStat),
                m_name,
                GraphicsContext::colorstr(Color::progressDone),
                achievement->title
            )
        );

        Log::info(formatString("Achievement unlocked by $+: $+", m_name, achievement->title));

        if (hasCompletedMilestone(milestoneId))
        {
            serverWorld->sendChatToAllPlayers(
                formatString(
                    "$+MILESTONE COMPLETED by $+: $+$+",
                    GraphicsContext::colorstr(Color::positiveStat),
                    m_name,
                    GraphicsContext::colorstr(Color::progressDone),
                    milestone.label
                )
            );

            Log::info(formatString("Milestone completed by $+: $+", m_name, milestone.label));
        }
    }
    else
    {
        writeChat(
            formatString(
                "$+ACHIEVEMENT UNLOCKED: $+$+",
                GraphicsContext::colorstr(Color::positiveStat),
                GraphicsContext::colorstr(Color::progressDone),
                achievement->title
            )
        );

        if (hasCompletedMilestone(milestoneId))
        {
            writeChat(
                formatString(
                    "$+MILESTONE COMPLETED: $+$+",
                    GraphicsContext::colorstr(Color::positiveStat),
                    GraphicsContext::colorstr(Color::progressDone),
                    milestone.label
                )
            );
        }
    }

    sendSerialMessage(MessageType::Achieved, milestoneId, (uint8_t) achievementId);
}

bool Player::getNoclip()
{
    return noclip;
}

bool Player::canDig(glm::ivec3 pos)
{
    // TODO: When on a non-local world, we need to send a message to the client to tell them
    // that a block is undiggable due to monitors.

    LocalWorld *localWorld = dynamic_cast<LocalWorld*>(world);
    if (localWorld != nullptr && !localWorld->canBreak(pos))
    {
        return false;
    }

    Block *block = world->getBlockAt(pos);
    if (block == nullptr)
    {
        return false;
    }

    return getTool()->canDig(holdingStack(), *block);
}

bool Player::isCrouching()
{
    return crouching;
}

void Player::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(
        outBytes,
        pos,
        vel,
        phi, theta,
        items,
        m_name,
        m_sprite,
        milestoneBitmaps,
        crouching,
        mapPins
    );
}

size_t Player::deserializeState(const uint8_t *data, size_t size)
{
    return deserialize(
        data, size,
        pos,
        vel,
        phi, theta,
        items,
        m_name,
        m_sprite,
        milestoneBitmaps,
        crouching,
        mapPins
    );
}

ItemStack& Player::holdingStack()
{
    return *selectedSlot;
}

int Player::getSelectedSlot()
{
    return selectedSlot.slotIndex();
}

std::string Player::getName()
{
    return m_name;
}

void Player::disconnectController()
{
    guiChannel.detach();
    movingStack = ItemStack();
}

void Player::sendPlayerMessage(MessageType type, const uint8_t *payload, size_t size)
{
    NestedPayload nested;
    deserialize(payload, size, nested);

    std::vector<uint8_t> message = serialize(type, nested);
    
    sendMessage(message.data(), message.size());
}

ITool* Player::getTool()
{
    ItemStack &stack = holdingStack();
    ITool *tool = stack.getItemAs<ITool>();

    if (tool != nullptr)
    {
        return tool;
    }

    return &handToolInstance;
}

void Player::onMessage(const void *msg, size_t size)
{
    if (size < 4)
    {
        return;
    }

    const MessageHeader *header = reinterpret_cast<const MessageHeader*>(msg);
    if (header->type == MessageType::SetSelectedSlot && size == sizeof(SetSelectedSlotMessage))
    {
        const SetSelectedSlotMessage *slotMessage = reinterpret_cast<const SetSelectedSlotMessage*>(msg);
        if (slotMessage->slot < 0 || slotMessage->slot >= numInventorySlots)
        {
            return;
        }

        selectedSlot = items.store("inventory").at(slotMessage->slot);
    }
    else if (header->type == MessageType::OpenInventory)
    {
        connectController<InventoryController>("main:PlayerInventoryController");
    }
    else if (header->type == MessageType::UpdateDigState && size == sizeof(UpdateDigStateMessage))
    {
        const UpdateDigStateMessage *digMessage = reinterpret_cast<const UpdateDigStateMessage*>(msg);
        m_digState = digMessage->digState;

        if (!m_digState.isDigging)
        {
            digAudioSource.setSound(Sound::NONE);
        }
        else
        {
            Block *block = world->getBlockAt(m_digState.pos);
            if (block == nullptr)
            {
                return;
            }

            Sound &diggingSound = block->canManuallyDig() ?
                block->getManualDiggingSound() :
                getTool()->getDigSoundFor(holdingStack(), *block);

            digAudioSource.setPosition(glm::vec3(m_digState.pos) + glm::vec3(0.5f));
            digAudioSource.setSound(diggingSound);
        }
    }
    else if (header->type == MessageType::Achieved)
    {
        MessageType type;
        std::string milestoneId;
        uint8_t achievementId;
        deserialize((const uint8_t*) msg, size, type, milestoneId, achievementId);

        milestoneBitmaps[milestoneId] |= static_cast<uint64_t>(1) << achievementId;

        if (hasCompletedMilestone(milestoneId))
        {
            Sound::milestoneCompleted.play();
        }
        else
        {
            Sound::achievement.play();
        }
    }
    else if (header->type == MessageType::OpenMap)
    {
        IEquipment::MapLevel mapLevel = IEquipment::MapLevel::None;
        for (const auto &stack : items.store("equipment"))
        {
            IEquipment *equipment = stack.getItemAs<IEquipment>();
            if (equipment == nullptr)
            {
                continue;
            }

            IEquipment::MapLevel thisMapLevel = equipment->getMapLevel();
            if (thisMapLevel > mapLevel)
            {
                mapLevel = thisMapLevel;
            }
        }

        if (mapLevel == IEquipment::MapLevel::None)
        {
            writeChat("You do not have a map equipped.", Color::error);
            return;
        }

        connectController<MapController>("main:MapController", mapLevel);
    }
    else
    {
        world->onPlayerMessage(*this, msg, size);
    }
}

void Player::draw(GraphicsContext &context, WorldDisplay &worldDisplay)
{
    // Render the player model if dirty.
    if (m_model == nullptr || m_modelDirty)
    {
        m_modelDirty = false;

        VoxelSpriteRenderer renderer(m_sprite.isSingularity() ? VoxelSprite::player : m_sprite);
        m_model = renderer.makeModel();
    }

    // Render the tag model if needed.
    if (tagModel == nullptr && !m_name.empty())
    {
        VoxelSprite tagSprite = VoxelSprite::fromText(
            Font::generic,
            m_name,
            {{0x00, 0x00, 0x00, 0xFF}},
            {{0xFF, 0xFF, 0xFF, 0xFF}}
        );

        VoxelSpriteRenderer renderer(tagSprite);
        tagModel = renderer.makeModel();

        glm::ivec3 tagSize = tagSprite.getSize();
        tagOffset = -glm::vec3(tagSize) / 16.0f;
    }

    // Draw the player model if necessary.
    if (worldDisplay.inThirdPersonView() && worldDisplay.getPlayer().get() == this)
    {
        drawModel(context, worldDisplay, *this);
    }
    else
    {
        world->drawPlayerModel(context, worldDisplay, *this);
    }

    // Draw the block selector if necessary.
    if (gadgetsEnabled)
    {
        world->drawBlockSelector(context, worldDisplay, *this);
    }

    // If we are digging, draw the cracks.
    if (m_digState.isDigging)
    {
        int stage = std::max(0, std::min(7, static_cast<int>(8 * (m_digState.currentDamage/m_digState.hardness))));

        std::vector<BoundingBox<wfixed>> boxes;
        Block *block = world
            ->getBlockAt(m_digState.pos);

        if (block != nullptr && stage != 0)
        {
            block->getCollisionBoxes(*world, m_digState.pos, boxes);
        
            for (BoundingBox<wfixed> &box : boxes)
            {
                worldDisplay.drawModelInBox(getCracks(stage), box);
            }
        }
    }

    // Emit our lights.
    float playerLightIntensity = world->getPlayerLightIntensity(*this);
    glm::dvec3 eye = getEyePos();
    glm::uvec3 eyeBlockPos = glm::uvec3(glm::ivec3(glm::floor(eye)));
    glm::vec3 blockVoxelOffset = 8.0f * glm::vec3(glm::fract(eye));

    glm::uvec3 posVoxel = (eyeBlockPos << 3u) + glm::uvec3(glm::ivec3(blockVoxelOffset));

    worldDisplay
        .getFrameDrawer()
        .lightingStage
        ->addEntityLight(
            LightingStage::LightSource(
                posVoxel,
                glm::vec3(playerLightIntensity) + getGlowColor()
            )
        );
}

void Player::sendChat(std::string chat)
{
    sendPlayerMessage(MessageType::SendChat, (const uint8_t*) chat.c_str(), chat.size());
}

bool Player::isHoldingScrewdriver()
{
    ItemStack &stack = holdingStack();
    if (stack.isEmpty())
    {
        return false;
    }

    return stack.getItem()->name == "main:screwdriver";
}

glm::vec3 Player::getGlowColor()
{
    glm::vec3 light(0.0f);

    int i;
    for (i=0; i<10; i++)
    {
        ItemStack stack = items.store("inventory").getStackInSlot(i);
        if (stack.isEmpty())
        {
            continue;
        }

        light += stack.getItem()->getGlowColor(stack) * (float) stack.getStackSize();
    }

    return light;
}

void Player::updateDiggingState(LocalWorld &world)
{
    bool digStateDirty = false;
    ITool *tool = getTool();

    if (!controlFlags.contains(Player::ControlFlag::Dig))
    {
        // Not trying to dig.
        if (m_digState.isDigging)
        {
            m_digState.isDigging = 0;
            digStateDirty = true;
        }
    }
    else
    {
        // Trying to dig, see where the ray falls.
        glm::dvec3 pos = getEyePos();

        World::BlockHit hit = this->world->castRay(pos, getForwardVector(), handRange);
        if (!hit.hasHit)
        {
            // Not hitting any block, stop digging.
            if (m_digState.isDigging)
            {
                m_digState.isDigging = 0;
                digStateDirty = true;
            }
        }
        else if (m_digState.isDigging && m_digState.pos == glm::ivec3(hit.coords))
        {
            // Hit the block we are already digging, increase damage.
            m_digState.currentDamage += m_digState.power;
            digStateDirty = true;

            // If we dug out the block, stop digging and break it.
            if (m_digState.currentDamage >= m_digState.hardness)
            {
                Block *existingBlock = this->world->getBlockAt(m_digState.pos);
                this->world->playSound(existingBlock->getBreakingSound(), m_digState.pos);
                
                m_digState.isDigging = 0;
                tool->harvest(holdingStack(), world, m_digState.pos);
            }
        }
        else
        {
            // Hit a block which we are not currently digging; start the dig.
            Block *block = this->world->getBlockAt(hit.coords);
            if (canDig(hit.coords))
            {
                m_digState.isDigging = 1;
                m_digState.pos = hit.coords;
                m_digState.currentDamage = 0.0f;
                m_digState.hardness = block->hardness;
                m_digState.power = tool->getPowerAgainst(holdingStack(), *block);
                digStateDirty = true;
            }
            else if (m_digState.isDigging)
            {
                m_digState.isDigging = 0;
                digStateDirty = true;
            }
        }
    }

    // If the digging state has changed, send it.
    if (digStateDirty)
    {
        sendDigUpdate();
    }
}

void Player::drawModel(GraphicsContext &context, WorldDisplay &worldDisplay, Player &ourPlayer)
{
    glm::vec3 translation = this->pos - ourPlayer.getEyePos();
    worldDisplay.getFrameDrawer().setModelMatrix(
        glm::translate(glm::mat4(1.0f), translation) *
        glm::rotate(glm::mat4(1.0f), this->theta, glm::vec3(0.0f, 0.0f, -1.0f)) *
        glm::translate(glm::mat4(1.0f), glm::vec3(-0.5f, -0.5f, 0.0f))
    );

    m_model->draw(context, worldDisplay);

    if (tagModel != nullptr)
    {
        glm::dvec3 towardsCamera = ourPlayer.getEyePos() - getEyePos();
        towardsCamera.z = 0.0;

        glm::vec3 towardsCameraNorm = glm::normalize(towardsCamera);

        glm::mat4 rotation = glm::mat4(1.0f);
        rotation[0] = glm::vec4(glm::cross(towardsCameraNorm, glm::vec3(0.0f, 0.0f, 1.0f)), 0.0f);
        rotation[1] = glm::vec4(towardsCameraNorm, 0.0f);

        worldDisplay.getFrameDrawer().setModelMatrix(
            glm::translate(glm::mat4(1.0f), translation + glm::vec3(0.0f, 0.0f, tagHeight)) *
            rotation *
            glm::scale(glm::mat4(1.0f), glm::vec3(0.5)) *
            glm::translate(glm::mat4(1.0f), tagOffset)
        );

        tagModel->draw(context, worldDisplay);
    }
}

void Player::drawBlockSelector(GraphicsContext &context, WorldDisplay &worldDisplay)
{
    // If a block is selected, draw the selector.
    glm::dvec3 eye = getEyePos();

    World::BlockHit hit = world->castRay(eye, getForwardVector(), handRange);
    if (hit.hasHit)
    {
        glm::vec4 selectionTint = canDig(hit.coords) ?
            Color::blockSelectionTintEnabled :
            Color::blockSelectionTintDisabled;
        
        std::vector<glm::ivec3> selectedBlocks;
        getTool()->getSelectedBlocks(
            holdingStack(),
            *this,
            *world,
            hit.coords,
            selectedBlocks
        );

        for (const auto &selectedBlockPos : selectedBlocks)
        {
            worldDisplay.getFrameDrawer().terrainStage->tintManager.setTintAt(selectedBlockPos, selectionTint);
        }
    }
}

void Player::writeChat(std::string text, glm::vec3 color)
{
    if (client != nullptr)
    {
        client->sendChat(text, color);
    }

    ChatEntry entry;
    entry.color = color;
    entry.text = text;
    entry.receiveTime = SDL_GetTicks();
    chatLog.push_back(entry);

    if (chatLog.size() == maxChatLogLines)
    {
        chatLog.pop_front();
    }
}

void Player::playFootstepSounds()
{
    walkingDistance += glm::length(glm::vec3(vel));

    BoundingBox<wfixed> box = getBoundingBox(pos);
    Block *blockUnder = world->getBlockAt(glm::floor(glm::dvec3(pos.x, pos.y, box.z0 - 0.1)));

    if (blockUnder != nullptr && walkingDistance > 4.0f)
    {
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_real_distribution<> distr(-1.0, 1.0);

        lastFootstepDirection = -lastFootstepDirection;
        glm::dvec3 footstepPos = glm::dvec3(pos.x, pos.y, box.z0 + 0.1);
        footstepPos += getRightVector() * 0.4f * (float) lastFootstepDirection;
        
        Sound &sound = blockUnder->getRandomFootstepSound();
        sound.play(footstepPos, vel, 1.0f + 0.1f * distr(gen), 1.0f + 0.1f * distr(gen));

        walkingDistance -= 4.0f;
    }

    if (blockUnder == nullptr)
    {
        resetFootstepTimer();
    }
}

void Player::resetFootstepTimer()
{
    walkingDistance = 4.0f;
}

void Player::onMapPinsUpdated()
{
    sendSerialMessage(MessageType::UpdateMapPins, mapPins);
}

bool Player::isGuiBusy()
{
    return guiChannel.currentController != nullptr && guiChannel.currentController->isBusy();
}

ControllerChannel* Player::getControllerChannel(uint8_t channelId)
{
    if (channelId >= Protocol::numControllerChannels)
    {
        return nullptr;
    }

    return ctrlChannelTable[channelId];
}

void Player::reattachGadget(size_t index)
{
    Item *currentItem = items.store("equipment").at(index)->getItem();
    if (currentItem == gadgetItems[index])
    {
        return;
    }

    gadgetItems[index] = currentItem;

    auto channel = getControllerChannel(1 + index);
    auto equipment = dynamic_cast<IEquipment*>(currentItem);

    if (equipment == nullptr)
    {
        channel->detach();
        return;
    }

    auto id = equipment->getGadgetControllerID();
    if (id.empty())
    {
        channel->detach();
        return;
    }

    auto &ctrl = static_cast<GadgetController&>(channel->attach(id, nextStamp++));
    ctrl.bindEquipmentSlot(index);
    ctrl.initLocal(*items.store("equipment").at(index));
}

BoundingBox<cint32> Player::getViewBox()
{
    wvec3 center = wvec3(glm::floor(pos)) & wvec3(~0xF);

    cint32 horizontalViewDistance = std::min((int) clientPrefs.horizontalViewDistance, settings.horizontalViewDistance);
    cint32 verticalViewDistance = std::min((int) clientPrefs.verticalViewDistance, settings.verticalViewDistance);

    BoundingBox<cint32> viewBox;
    viewBox.x0 = (center.x - cint32(16) * horizontalViewDistance) & cint32(~0xF);
    viewBox.x1 = (center.x + cint32(16) * horizontalViewDistance + cint32(16)) & cint32(~0xF);
    viewBox.y0 = (center.y - cint32(16) * horizontalViewDistance) & cint32(~0xF);
    viewBox.y1 = (center.y + cint32(16) * horizontalViewDistance + cint32(16)) & cint32(~0xF);
    viewBox.z0 = (center.z - cint32(16) * verticalViewDistance) & cint32(~0xF);
    viewBox.z1 = (center.z + cint32(16) * verticalViewDistance + cint32(16)) & cint32(~0xF);

    return viewBox;
}

int Player::pullFluidFromEquipment(std::string fluidName, int maxAmount)
{
    int pulled = 0;

    for (auto &stack : items.store("equipment"))
    {
        if (pulled == maxAmount)
        {
            break;
        }

        IEquipment *equipment = stack.getItemAs<IEquipment>();
        if (equipment == nullptr)
        {
            continue;
        }

        pulled += equipment->pullFluidForEquipment(stack, fluidName, maxAmount - pulled);
    }

    return pulled;
}

bool Player::isUsingItemDirectly()
{
    return controlFlags.contains(ControlFlag::Crouch);
}

void Player::onBlockPlaced(std::string blockName, glm::ivec3 pos)
{
    playerTriggerRegistry.raise(*this, formatString("main:BlockPlaced::$+", blockName));

    LocalWorld *world = static_cast<LocalWorld*>(this->world);
    auto machine = world->getMachine(pos);

    if (machine == nullptr || !machine->hasComponent<AttributionComponent>())
    {
        return;
    }

    auto &attribComp = machine->getComponent<AttributionComponent>();
    attribComp.attribute(std::string(Machine::Action::Placed), *this);
}