#include <BoundingBox/BoundingBox.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <Entity/MiningRigEntity/MiningRigEntity.hpp>
#include <Machine/MMiningRig/MMiningRig.hpp>
#include <Serialize/Base.hpp>
#include <Utilities/FixedPoint.hpp>
#include <VoxelSprite/VoxelSprite.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <memory>

void MiningRigEntity::drawElement(GraphicsContext &context, WorldDisplay &display, Model &model, glm::ivec3 pos)
{
    glm::vec3 translation = wfvec3(pos) - display.getPlayer()->getEyePos();
    display.getFrameDrawer().setModelMatrix(glm::translate(glm::mat4(1.0f), translation));

    model.draw(context, display);
}

MiningRigEntity::MiningRigEntity(World &world, uint64_t id, glm::ivec3 machinePos) :
    Entity(world, "main:miningRigEntity", id),
    machinePos(machinePos)
{}

MiningRigEntity::MiningRigEntity(
    World &world,
    std::string typeId,
    uint64_t id
) : Entity(world, typeId, id)
{}

void MiningRigEntity::update()
{
    if (world->isLocal())
    {
        std::shared_ptr<MMiningRig> rig = rigMachine.lock();
        if (rig == nullptr)
        {
            if (initialized)
            {
                // We were already initialized and now the rig is null, that means it's detached.
                despawn();
                return;
            }

            LocalWorld *localWorld = static_cast<LocalWorld*>(world);
            rig = std::dynamic_pointer_cast<MMiningRig>(localWorld->getMachine(machinePos));

            if (rig == nullptr)
            {
                crash(formatString("Sanity check failed: No mining rig machine at expected position $+", machinePos));
            }

            rigMachine = rig;

            initialized = true;
            drillAreaBB = rig->getDrillAreaBB();
            drillPos = rig->getDrillPos();

            sendSerialMessage(MessageType::Init, drillAreaBB, drillPos);
        }

        if (rig->isDetached())
        {
            despawn();
            return;
        }

        glm::ivec3 currentDrillPos = rig->getDrillPos();
        if (drillPos != currentDrillPos)
        {
            drillPos = currentDrillPos;
            sendSerialMessage(MessageType::Update, drillPos);
        }
    }
}

void MiningRigEntity::draw(GraphicsContext &context, WorldDisplay &display)
{
    if (!initialized)
    {
        return;
    }

    BoundingBox<int> viewBox = (BoundingBox<int>) display.getPlayer()->getViewBox();
    if (viewBox.overlaps(drillAreaBB))
    {
        glm::ivec3 axelPos = glm::ivec3(0, drillPos.y, drillAreaBB.z0);
        for (axelPos.x = drillAreaBB.x0; axelPos.x < drillAreaBB.x1; axelPos.x++)
        {
            if (axelPos.x == drillPos.x)
            {
                continue;
            }

            drawElement(context, display, *Model::miningRigAxelX, axelPos);
        }

        axelPos = glm::ivec3(drillPos.x, 0, drillAreaBB.z0);
        for (axelPos.y = drillAreaBB.y0; axelPos.y < drillAreaBB.y1; axelPos.y++)
        {
            if (axelPos.y == drillPos.y)
            {
                continue;
            }

            drawElement(context, display, *Model::miningRigAxelY, axelPos);
        }

        glm::ivec3 jointPos = drillPos;
        jointPos.z = drillAreaBB.z0;

        drawElement(context, display, *Model::miningRigJoint, jointPos);
    }

    int startZ = std::max(viewBox.z0, drillPos.z + 1);
    int endZ = std::min(viewBox.z1, drillAreaBB.z0);

    glm::ivec3 pipePos = drillPos;
    for (pipePos.z = startZ; pipePos.z < endZ; pipePos.z++)
    {
        if (pipePos.z == drillPos.z + 1)
        {
            drawElement(context, display, *Model::miningRigLaser, pipePos);
        }
        else
        {
            drawElement(context, display, *Model::miningRigPipe, pipePos);
        }
    }
}

void MiningRigEntity::onMessage(const void *msg, size_t size)
{
    MessageType type = *((MessageType*) msg);
    if (type == MessageType::Init)
    {
        MessageType ignore;
        deserialize((const uint8_t*) msg, size, ignore, drillAreaBB, drillPos);
        initialized = true;
    }
    else if (type == MessageType::Update)
    {
        MessageType ignore;
        deserialize((const uint8_t*) msg, size, ignore, drillPos);
    }
}

void MiningRigEntity::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, initialized, drillAreaBB, drillPos);
}

size_t MiningRigEntity::deserializeState(const uint8_t *data, size_t size)
{
    return deserialize(data, size, initialized, drillAreaBB, drillPos);
}