#include <Belt/Belt.hpp>
#include <World/LocalWorld/LocalWorld.hpp>

void Belt::advance(LocalWorld &world)
{
    int lastOffset = beltBlockLength * 8 - 1;

    if (elements.size() != 0 && (int) (elements.back().abstractPos + beltOffset) >= lastOffset)
    {
        Element &lastElement = elements.back();
        glm::ivec3 endPos = getEndPos();
        world.beltManager.tryInsertInto(endPos, dir.opposite(), lastElement.itemStack);

        if (!lastElement.itemStack.isEmpty())
        {
            for (auto element = elements.rbegin(); element != elements.rend(); ++element)
            {
                if (element != elements.rbegin())
                {
                    auto prevElement = element;
                    prevElement--;

                    if (element->abstractPos + elementVoxelSpan <= prevElement->abstractPos)
                    {
                        break;
                    }
                }

                element->abstractPos--;
            }
        }
        else
        {
            elements.pop_back();
        }

        elementUpdateNum++;
    }

    beltOffset++;
}

glm::ivec3 Belt::getEndPos()
{
    glm::ivec3 endPos = startPos;
    endPos[dir.axis()] += dir.sign() * beltBlockLength;
    return endPos;
}

std::optional<int> Belt::getBlockOffset(glm::ivec3 pos)
{
    for (int i = 0; i < 3; i++)
    {
        if (i != dir.axis() && startPos[i] != pos[i])
        {
            return std::nullopt;
        }
    }

    uint32_t distance = (pos[dir.axis()] - startPos[dir.axis()]) * dir.sign();
    if (distance < beltBlockLength)
    {
        return distance;
    }

    return std::nullopt;
}

bool Belt::hasItems()
{
    return elements.size() != 0;
}

int Belt::getWeight()
{
    return std::max(1, ((int) beltBlockLength + 15) / 16 + ((int) elements.size() + 15) / 16);
}

void Belt::tryInsertAt(int voxelOffset, ItemStack &stack)
{
    if (stack.isEmpty())
    {
        return;
    }

    uint32_t newAbstractPos = (uint32_t) voxelOffset - beltOffset;

    std::list<Element>::iterator prevElement;
    for (prevElement=elements.begin(); prevElement!=elements.end(); ++prevElement)
    {
        int realOffset = (int) (prevElement->abstractPos + beltOffset);
        if (std::abs(realOffset - voxelOffset) < 4)
        {
            // Overlap with an existing stack, cannot insert.
            return;
        }

        if (realOffset >= voxelOffset)
        {
            break;
        }
    }

    ItemStack newStack = stack.extractOne();
    if (newStack.isEmpty())
    {
        return;
    }

    elements.insert(prevElement, {newAbstractPos, newStack});
    elementUpdateNum++;
}

bool Belt::operator==(const Belt &other) const
{
    return id == other.id &&
        startPos == other.startPos &&
        dir == other.dir &&
        isOperational == other.isOperational &&
        beltBlockLength == other.beltBlockLength &&
        elementUpdateNum == other.elementUpdateNum;
}