#include <Chunk/Chunk.hpp>
#include <ChunkWindow/ChunkWindow.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Item/Item.hpp>
#include <Settings/Settings.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>

void ChunkWindow::requestChunk(glm::ivec3 base)
{
    double distance = glm::length(glm::dvec3(player->pos - wfvec3(base)));

    ChunkHandle chunk = world.requestChunk(base, nullptr, (float) distance / (32.0 * 16.0));
    if (chunk->isEncoding())
    {
        return;
    }

    if (chunk->isInWindowLoadingQueue)
    {
        return;
    }

    chunk->isInWindowLoadingQueue = true;
    loadingChunks.push_back(chunk);
}

void ChunkWindow::requestBox(const BoundingBox<cint32> &box)
{
    glm::ivec3 base;
    for (base.z = box.z0; base.z != box.z1; base.z+=16)
    {
        for (base.y = box.y0; base.y != box.y1; base.y+=16)
        {
            for (base.x = box.x0; base.x != box.x1; base.x+=16)
            {
                requestChunk(base);
            }
        }
    }
}

void ChunkWindow::detachAll()
{
    for (ChunkHandle &chunk : loadingChunks)
    {
        chunk->isInWindowLoadingQueue = false;
    }

    for (ChunkHandle &chunk : visibleChunks)
    {
        chunk->detach();
    }

    loadingChunks.clear();
    visibleChunks.clear();
    chunksWithEffects.clear();
}

void ChunkWindow::checkLoadingChunks()
{
    for (size_t i = 0; i < loadingChunks.size(); i++)
    {
        ChunkHandle chunk = loadingChunks[i];
        if (chunk->isReady())
        {
            if (i != loadingChunks.size() - 1)
            {
                loadingChunks[i] = loadingChunks[loadingChunks.size() - 1];
            }

            loadingChunks.resize(loadingChunks.size() - 1, chunk);
            chunk->beginGridGeneration(*this);
            i--;

            chunk->isInWindowLoadingQueue = false;

            double distanceFromPlayer = glm::length(glm::vec3(player->pos - wfvec3(chunk->base)));
            int bucketIndex = std::min((int) encodingChunks.size() - 1, (int) std::floor(distanceFromPlayer / 16.0));
            encodingChunks[bucketIndex].push_back(chunk);
        }
    }
}

void ChunkWindow::checkEncodingChunks()
{
    int encodedChunks = 0;
    for (auto &bucket : encodingChunks)
    {
        for (size_t i = 0; i < bucket.size() && encodedChunks < 32; i++)
        {
            ChunkHandle chunk = bucket[i];
            if (!isChunkInView(*chunk) || chunk->tryEncodeIntoSVO())
            {
                if (i != bucket.size() - 1)
                {
                    bucket[i] = bucket[bucket.size() - 1];
                }

                bucket.resize(bucket.size() - 1, chunk);
                i--;
                encodedChunks++;

                if (isChunkInView(*chunk))
                {
                    visibleChunks.push_back(chunk);

                    if (chunk->hasBlockEffects())
                    {
                        chunksWithEffects.push_back(chunk);
                    }
                }
                else
                {
                    chunk->detach();
                }
            }
        }
    }
}

bool ChunkWindow::isChunkInView(Chunk &chunk)
{
    BoundingBox<cint32> chunkBox{
        chunk.base.x, chunk.base.y, chunk.base.z,
        chunk.base.x + 16, chunk.base.y + 16, chunk.base.z + 16,
    };

    return visibleBox.overlaps(chunkBox);
}

void ChunkWindow::shrinkToView(const BoundingBox<cint32> &box)
{
    bool shrank = false;
    if (visibleBox.x0 < box.x0)
    {
        visibleBox.x0 = box.x0;
        shrank = true;
    }

    if (visibleBox.x1 > box.x1)
    {
        visibleBox.x1 = box.x1;
        shrank = true;
    }

    if (visibleBox.y0 < box.y0)
    {
        visibleBox.y0 = box.y0;
        shrank = true;
    }

    if (visibleBox.y1 > box.y1)
    {
        visibleBox.y1 = box.y1;
        shrank = true;
    }

    if (visibleBox.z0 < box.z0)
    {
        visibleBox.z0 = box.z0;
        shrank = true;
    }

    if (visibleBox.z1 > box.z1)
    {
        visibleBox.z1 = box.z1;
        shrank = true;
    }

    if (shrank)
    {
        for (size_t i = 0; i < loadingChunks.size(); i++)
        {
            ChunkHandle chunk = loadingChunks[i];
            if (!isChunkInView(*chunk))
            {
                if (i != loadingChunks.size() - 1)
                {
                    loadingChunks[i] = loadingChunks[loadingChunks.size() - 1];
                }

                loadingChunks.resize(loadingChunks.size() - 1, loadingChunks[0]);
                chunk->isInWindowLoadingQueue = false;
                i--;
            }
        }

        for (size_t i = 0; i < visibleChunks.size(); i++)
        {
            ChunkHandle chunk = visibleChunks[i];
            if (!isChunkInView(*chunk))
            {
                if (i != visibleChunks.size() - 1)
                {
                    visibleChunks[i] = visibleChunks[visibleChunks.size() - 1];
                }

                visibleChunks.resize(visibleChunks.size() - 1, visibleChunks[0]);
                i--;

                chunk->detach();
            }
        }

        for (size_t i = 0; i < chunksWithEffects.size(); i++)
        {
            std::shared_ptr<Chunk> chunk = chunksWithEffects[i];
            if (!isChunkInView(*chunk))
            {
                if (i != chunksWithEffects.size() - 1)
                {
                    chunksWithEffects[i] = chunksWithEffects[chunksWithEffects.size() - 1];
                }

                chunksWithEffects.resize(chunksWithEffects.size() - 1, chunksWithEffects[0]);
                i--;
            }
        }
    }
}

void ChunkWindow::expandToView(const BoundingBox<cint32> &box)
{
    if (visibleBox.x0 > box.x0)
    {
        BoundingBox<cint32> loadBox = visibleBox;
        loadBox.x0 = box.x0;
        loadBox.x1 = visibleBox.x0;
        
        visibleBox.x0 = box.x0;
        requestBox(loadBox);
    }

    if (visibleBox.x1 < box.x1)
    {
        BoundingBox<cint32> loadBox = visibleBox;
        loadBox.x0 = visibleBox.x1;
        loadBox.x1 = box.x1;
        
        visibleBox.x1 = box.x1;
        requestBox(loadBox);
    }

    if (visibleBox.y0 > box.y0)
    {
        BoundingBox<cint32> loadBox = visibleBox;
        loadBox.y0 = box.y0;
        loadBox.y1 = visibleBox.y0;
        
        visibleBox.y0 = box.y0;
        requestBox(loadBox);
    }

    if (visibleBox.y1 < box.y1)
    {
        BoundingBox<cint32> loadBox = visibleBox;
        loadBox.y0 = visibleBox.y1;
        loadBox.y1 = box.y1;
        
        visibleBox.y1 = box.y1;
        requestBox(loadBox);
    }

    if (visibleBox.z0 > box.z0)
    {
        BoundingBox<cint32> loadBox = visibleBox;
        loadBox.z0 = box.z0;
        loadBox.z1 = visibleBox.z0;
        
        visibleBox.z0 = box.z0;
        requestBox(loadBox);
    }

    if (visibleBox.z1 < box.z1)
    {
        BoundingBox<cint32> loadBox = visibleBox;
        loadBox.z0 = visibleBox.z1;
        loadBox.z1 = box.z1;
        
        visibleBox.z1 = box.z1;
        requestBox(loadBox);
    }
}

void ChunkWindow::rescanParticlePool()
{
    particlePoolScannedThisFrame = true;

    size_t actualSize = 0;
    for (size_t i = 0; i < particlePool.size(); i++)
    {
        const auto &particle = particlePool[i];
        if (!particle.isAlive())
        {
            if (particle.endTime != particle.startTime)
            {
                particleReclaimStack.push_back(i);
                particlePool.replace(i, ParticlesStage::Particle{});
            }
        }
        else
        {
            actualSize = i + 1;
        }
    }

    particlePool.resize(actualSize);
}

ChunkWindow::ChunkWindow(World &world, std::shared_ptr<Player> player, WorldDisplay &worldDisplay) :
    visibleBox{},
    particlePool(GraphicsContext::instance->getDevice()),
    world(world),
    player(player),
    svo(GraphicsContext::instance->getDevice()),
    worldDisplay(worldDisplay)
{}

void ChunkWindow::setVisibleBox(const BoundingBox<cint32> &newBox)
{
    if (visibleBox.getVolume() == 0 || !visibleBox.overlaps(newBox))
    {
        detachAll();
        requestBox(newBox);
        visibleBox = newBox;
    }
    else
    {
        shrinkToView(newBox);
        expandToView(newBox);
    }
}

void ChunkWindow::update(WorldDisplay &display)
{
    particlePoolScannedThisFrame = false;

    checkLoadingChunks();
    checkEncodingChunks();

    for (size_t i = 0; i < chunksWithEffects.size(); i++)
    {
        std::shared_ptr<Chunk> chunk = chunksWithEffects[i];
        currentBaseVoxel = glm::uvec3(chunk->base) << 3u;
        chunk->updateEffects(*this);

        if (!chunk->hasBlockEffects())
        {
            if (i != chunksWithEffects.size() - 1)
            {
                chunksWithEffects[i] = chunksWithEffects[chunksWithEffects.size() - 1];
            }

            chunksWithEffects.resize(chunksWithEffects.size() - 1, chunksWithEffects[0]);
            i--;
        }
    }

    currentBaseVoxel = glm::uvec3(glm::floor(player->pos * wfvec3(8.0, 8.0, 8.0)));
    world.weather.applyEffects(*this);
}

void ChunkWindow::draw(WorldDisplay &display)
{
    if (particlePool.size() != 0)
    {
        display.getFrameDrawer().particlesStage->draw(
            particlePool.getDescSet(GraphicsContext::instance->cmdbuf()),
            particlePool.size()
        );
    }
}

void ChunkWindow::purge()
{
    detachAll();
    encodingChunks = {};
    player = nullptr;
}

void ChunkWindow::emitParticle(ParticlesStage::Particle particle)
{
    particle.baseVoxel = currentBaseVoxel;
    if (particle.endTime == particle.startTime)
    {
        crash("Particle spawned with the same start and end times!");
    }

    if (!particlePoolScannedThisFrame && particleReclaimStack.size() == 0)
    {
        rescanParticlePool();
    }

    if (particleReclaimStack.size() == 0)
    {
        particlePool.append(particle);
        return;
    }

    size_t index = particleReclaimStack[particleReclaimStack.size()-1];
    particleReclaimStack.pop_back();
    particlePool.replace(index, particle);
}

void ChunkWindow::addChunkWithEffects(std::shared_ptr<Chunk> chunk)
{
    if (isChunkInView(*chunk))
    {
        chunksWithEffects.push_back(chunk);
    }
}

LightingStage::LightID ChunkWindow::addBlockLight(LightingStage::LightSource lightSource)
{
    return worldDisplay.getFrameDrawer().lightingStage->addBlockLight(lightSource);
}

void ChunkWindow::setBlockLight(LightingStage::LightID id, LightingStage::LightSource lightSource)
{
    worldDisplay.getFrameDrawer().lightingStage->setBlockLight(id, lightSource);
}

void ChunkWindow::releaseBlockLight(LightingStage::LightID id)
{
    worldDisplay.getFrameDrawer().lightingStage->releaseBlockLight(id);
}

size_t ChunkWindow::getActiveLights()
{
    return worldDisplay.getFrameDrawer().lightingStage->getActiveLights();
}