#include <BeltView/BeltView.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <Chunk/Chunk.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <Block/SpriteBlock/ConveyorBelt/ConveyorBelt.hpp>
#include <ChunkWindow/ChunkWindow.hpp>
#include <memory>

void BeltView::advance()
{
    advanceCounter++;
    if ((advanceCounter % 6) == 0)
    {
        int lastOffset = state.beltBlockLength * 8 - 1;
        if (state.elements.size() != 0 && (int) (state.elements.back().abstractPos + state.beltOffset) >= lastOffset)
        {
            return;
        }

        state.beltOffset++;
    }
}

bool BeltView::needsUpdate(const Belt& belt, bool isActive) const
{
    if (!isActive && state.beltOffset != belt.beltOffset)
    {
        return true;
    }

    return state != belt || isActive != this->isActive;
}

void BeltView::draw(WorldDisplay &display, const Frustum &viewingFrustum)
{
    GraphicsContext &context = *GraphicsContext::instance;

    glm::dvec3 corner1 = state.startPos;
    glm::dvec3 corner2 = corner1;
    corner2 += glm::dvec3(((int) state.beltBlockLength + 1) * state.dir.out());

    glm::dvec3 start = glm::min(corner1, corner2);
    glm::dvec3 end = glm::max(corner1, corner2);
    end += glm::vec3(1.0, 1.0, 2.0);

    BoundingBox<double> bbox{start.x, start.y, start.z, end.x, end.y, end.z};
    if (!viewingFrustum.containsBox(bbox))
    {
        return;
    }

    // Draw the elements.
    for (Belt::Element &element : state.elements)
    {
        double voxelOffset = static_cast<double>(element.abstractPos + state.beltOffset) / 8.0;
        glm::dvec3 pos = glm::dvec3(state.startPos) + voxelOffset * glm::dvec3(state.dir.out());
        pos[state.dir.axis()] += 0.25;
        pos[state.dir.axis() ^ 1] += 0.5;
        pos.z += 3.0 / 8.0;

        if (state.dir.sign() < 0)
        {
            pos[state.dir.axis()] += 0.5;
        }

        glm::mat4 matrix = glm::translate(glm::mat4(1.0f), glm::vec3(pos - glm::dvec3(display.player->getEyePos())));
        element.itemStack.getItem()->drawFlat(context, display, matrix, element.itemStack);
    }

    // Draw the belts.
    glm::ivec3 startBlockPos = state.startPos;
    uint32_t length = state.beltBlockLength;

    if (state.isOperational)
    {
        startBlockPos += state.dir.out();
        length--;
    }

    if (length == 0)
    {
        return;
    }
}

void BeltView::updateAnimation(WorldDisplay &display, const Frustum &viewingFrustum, uint32_t spriteId)
{
    glm::dvec3 corner1 = state.startPos;
    glm::dvec3 corner2 = corner1;
    corner2 += glm::dvec3(((int) state.beltBlockLength + 1) * state.dir.out());

    glm::dvec3 start = glm::min(corner1, corner2);
    glm::dvec3 end = glm::max(corner1, corner2);
    end += glm::vec3(1.0, 1.0, 2.0);

    if (state.startPos != lastKnownStartPos || state.beltBlockLength != lastBeltBlockLength)
    {
        std::shared_ptr<Chunk> chunk;
        lastKnownStartPos = state.startPos;
        lastBeltBlockLength = state.beltBlockLength;

        for (uint32_t i = 0; i < state.beltBlockLength; i++)
        {
            glm::ivec3 pos = state.startPos + glm::ivec3(i) * state.dir.out();
            glm::ivec3 chunkBase = pos & glm::ivec3(~0xF);
            if (chunk == nullptr || chunk->base != chunkBase)
            {
                chunk = display.getWorld()->getChunk(chunkBase);
            }
            
            if (chunk == nullptr)
            {
                continue;
            }

            chunk->refreshBlock(pos & glm::ivec3(0xF));
        }
    }

    BoundingBox<double> bbox{start.x, start.y, start.z, end.x, end.y, end.z};
    if (!viewingFrustum.containsBox(bbox))
    {
        return;
    }

    uint8_t currentState = state.dir.asNumber() & 3;
    currentState |= ((state.beltOffset & 3) << 2);

    if (isActive)
    {
        currentState |= (1 << 4);
    }

    BlockAtlas::SpriteID beltSprite = dynamic_cast<ConveyorBelt&>(blockRegistry.getBlockByName("main:conveyorBelt"))
        .getBeltSpriteForMeta(currentState);
    
    for (size_t i = 0; i < 8; i++)
    {
        display.world->window->svo.setChildNode(spriteId, i, display.world->window->svo.getChildNode(beltSprite, i));
    }
}