#include <Utilities/FixedPoint.hpp>
#include <chrono>
#include <Entity/PhysicsEntity/ItemEntity/ItemEntity.hpp>
#include <World/World.hpp>
#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <Block/Block.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <PlayerTriggerRegistry/PlayerTriggerRegistry.hpp>

using namespace std::chrono_literals;

ItemEntity::ItemEntity(World &world, std::string typeId, uint64_t id) :
    PhysicsEntity(world, typeId, id),
    spawnTime(std::chrono::steady_clock::now())
{}

ItemEntity::ItemEntity(World &world, uint64_t id, wfvec3 posArg, wfvec3 velArg, ItemStack stack) :
    PhysicsEntity(world, "main:itemEntity", id, posArg, velArg),
    stack(stack),
    spawnTime(std::chrono::steady_clock::now())
{
    pos = posArg;
    vel = velArg;
}

BoundingBox<wfixed> ItemEntity::getBoundingBox(wfvec3 pos)
{
    BoundingBox<wfixed> box;
    box.x0 = pos[0] - wfixed(0.4);
    box.x1 = pos[0] + wfixed(0.4);
    box.y0 = pos[1] - wfixed(0.4);
    box.y1 = pos[1] + wfixed(0.4);
    box.z0 = pos[2] - wfixed(0.4);
    box.z1 = pos[2] + wfixed(0.4);
    return box;
}

void ItemEntity::update()
{
    bool dirty = false;
    if (std::chrono::steady_clock::now() - spawnTime >= 0.5s)
    {
        world->forEachEntity([this, &dirty](std::shared_ptr<Entity> entity)
        {
            if (stack.isEmpty())
            {
                return;
            }

            std::shared_ptr<Player> player = std::dynamic_pointer_cast<Player>(entity);
            if (player != nullptr && player->items.store("inventory").canInsertStack(stack))
            {
                if (world->isLocal())
                {
                    if (glm::length(glm::vec3(player->pos - pos)) <= player->pickupRadius)
                    {
                        stack.onCollectedBy(*player);
                        player->items.store("inventory").insertStack(stack);
                        world->playSound(Sound::collect, glm::ivec3(glm::floor(player->pos)));
                        dirty = true;
                    }
                }

                if (glm::length(glm::vec3(player->pos - pos)) <= player->pickupRadius * 1.5f)
                {
                    glm::dvec3 dir = glm::normalize(glm::dvec3(player->pos - pos));
                    pos += 0.2 * dir;
                }
            }
        });
    }

    PhysicsEntity::update();

    if (world->isLocal())
    {
        if (updateCheckNeeded)
        {
            updateCheckNeeded = false;

            if (!lastUpdatePos.has_value() || glm::length(glm::vec3(*lastUpdatePos - pos)) > 0.08)
            {
                lastUpdatePos = pos;
                dirty = true;
            }
        }

        if (dirty && !stack.isEmpty())
        {
            std::vector<uint8_t> data;
            serializeState(data);
            sendMessage(data.data(), data.size());
        }

        if (stack.isEmpty())
        {
            despawn();
        }
        else if (std::chrono::steady_clock::now() - spawnTime >= despawnAfter)
        {
            despawn();
        }
    }
}

glm::vec3 ItemEntity::getFrictionCoefficients()
{
    return glm::vec3(5.0f);
}

void ItemEntity::draw(GraphicsContext &context, WorldDisplay &display)
{
    Item *item = stack.getItem();
    if (item == nullptr)
    {
        return;
    }

    item->draw3D(context, display, pos - display.player->getEyePos(), stack);
}

void ItemEntity::onMessage(const void *msg, size_t size)
{
    deserializeState((const uint8_t*) msg, size);
}

void ItemEntity::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, pos, vel, stack);
}

size_t ItemEntity::deserializeState(const uint8_t *data, size_t size)
{
    return deserialize(data, size, pos, vel, stack);
}

void ItemEntity::onEnteringUnloadedChunk()
{
    if (world->isLocal())
    {
        despawn();
    }
}

bool ItemEntity::onCollision()
{
    updateCheckNeeded = true;
    return false;
}