#include <cstring>

#include <Chunk/Chunk.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <World/World.hpp>
#include <Utilities/Utilities.hpp>
#include <Utilities/Async.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <Machine/Machine.hpp>
#include <Utilities/Perf.hpp>
#include <WorldGridNode/WorldGridNode.hpp>
#include <Side/Side.hpp>
#include <ChunkEncoder/ChunkEncoder.hpp>

void Chunk::regenerateGrid()
{
    dirty = false;
    gridGenerated = false;
    std::shared_ptr<ChunkEncoder> encoder = this->encoder;

    std::shared_ptr<Chunk> us = shared_from_this();
    DenseArray variantPalette = this->variantPalette;
    DenseArray blockGrid = this->blockGrid;

    dispatchTask([this, encoder, us, variantPalette, blockGrid]
    {
        encoder->generateGrid(variantPalette, blockGrid);
        gridGenerated = true;
    }, encoder->getEncodingRelativeDeadline(), PriorityClass::ChunkRender);
}

const std::string Chunk::gridFileBaseName = "blocks.dat";

Chunk::Chunk(std::shared_ptr<WorldGridNode> node, glm::ivec3 base) :
    blockGrid(0, 16 * 16 * 16),
    loadingInProgress(true),
    node(node),
    base(base),
    dirty(true)
{
    if (node == nullptr)
    {
        gridFilePath = formatString("wgrid/lvl0/x$+/y$+/z$+/$+", base.x, base.y, base.z, gridFileBaseName);
    }
}

Chunk::Chunk(const Chunk &other) :
    variantPalette(other.variantPalette),
    blockGrid(other.blockGrid),
    loadingInProgress(other.loadingInProgress.load()),
    node(other.node),
    gridFilePath(other.gridFilePath),
    base(other.base),
    dirty(true)
{}

std::tuple<int32_t, uint8_t> Chunk::getBlockAndMeta(glm::ivec3 offset)
{
    size_t index = 16 * 16 * offset.z + 16 * offset.y + offset.x;
    uint32_t paletteIndex = blockGrid.get(index);

    uint32_t variant = paletteIndex;
    if (variantPalette.getNumEntries() != 0)
    {
        variant = variantPalette.get(paletteIndex);
    }

    return std::make_tuple(static_cast<int32_t>(variant >> 8), static_cast<uint8_t>(variant));
}

void Chunk::setBlockAndMeta(glm::ivec3 offset, int32_t blockId, uint8_t meta)
{
    uint32_t variant = (static_cast<uint32_t>(blockId) << 8) | static_cast<uint32_t>(meta);
    size_t index = 16 * 16 * offset.z + 16 * offset.y + offset.x;

    if (variantPalette.getNumEntries() == 0)
    {
        if (blockGrid.getBitsPerEntry() > 0)
        {
            blockGrid.set(index, variant);
        }

        if (blockGrid.getBitsPerEntry() == 0)
        {
            variantPalette.push(0);
            variantPalette.push(variant);
            blockGrid.set(index, 1);
        }

        return;
    }

    size_t paletteIndex;
    std::optional<size_t> paletteIndexOpt = variantPalette.find(variant);

    if (paletteIndexOpt.has_value())
    {
        paletteIndex = paletteIndexOpt.value();
    }
    else if (variantPalette.getNumEntries() >= maxVariantsInPalette)
    {
        size_t bitsPerBlock = 0;
        size_t numBlocks = blockRegistry.getTotalBlocks();

        while (numBlocks > (1 << bitsPerBlock))
        {
            bitsPerBlock++;
        }

        auto &variantPalette = this->variantPalette;
        blockGrid.transform(
            bitsPerBlock + 8,
            [&variantPalette](uint32_t i)
            {
                return variantPalette.get(i);
            }
        );

        variantPalette.clear();
        paletteIndex = variant;
    }
    else
    {
        paletteIndex = variantPalette.getNumEntries();
        variantPalette.push(variant);
    }

    blockGrid.set(index, paletteIndex);
}

void Chunk::toNetworkPayload(std::vector<uint8_t> &bytes)
{
    serializeInto(bytes, variantPalette, blockGrid);
}

void Chunk::fromNetworkPayload(const uint8_t *bytes, size_t size)
{
    auto oldVariantPalette = variantPalette;
    auto oldBlockGrid = blockGrid;

    deserialize(bytes, size, variantPalette, blockGrid);
    
    if (encoder != nullptr && gridGenerated)
    {
        if (oldVariantPalette == variantPalette && oldBlockGrid == blockGrid)
        {
            // Nothing changed.
            return;
        }

        // TODO: Instead of refreshing every block (really slow), let's investigate whether we can
        //       simply invalidate the chunk and put it back in the "encoding" queue.
        glm::ivec3 offset;
        for (offset.z = 0; offset.z < 16; offset.z++)
        {
            for (offset.y = 0; offset.y < 16; offset.y++)
            {
                for (offset.x = 0; offset.x < 16; offset.x++)
                {
                    encoder->refreshBlock(base + offset);
                }
            }
        }
    }
    else if (loadingInProgress)
    {
        markLoadingComplete();
    }
    else
    {
        dirty = true;
    }
}

void Chunk::markLoadingComplete()
{
    // Mark this chunk as no longer in progress, and needs render.
    dirty = true;
    loadingInProgress = false;
}

void Chunk::deserializeGrid(std::vector<uint8_t> &bytes)
{
    deserialize(bytes.data(), bytes.size(), variantPalette, blockGrid, monitorPositions);
}

bool Chunk::purgeVoxels()
{
    if (encoder != nullptr)
    {
        if (!gridGenerated)
        {
            return false;
        }

        detach();
    }

    return true;
}

std::shared_ptr<WorldGridNode> Chunk::getNode()
{
    return node.lock();
}

void Chunk::save(DataUpdatePlanner &planner)
{
    needsSaving = false;
    lastFlushGeneration = planner.getCurrentGeneration();
    saveInto(planner.newFile(getGridFilePath()));
}

void Chunk::saveInto(std::vector<uint8_t> &data)
{
    serializeInto(data, variantPalette, blockGrid, monitorPositions);
}

const std::string& Chunk::getGridFilePath()
{
    if (gridFilePath.size() == 0)
    {
        gridFilePath = formatString("$+/$+", getNode()->getBaseDir(), gridFileBaseName);
    }

    return gridFilePath;
}

bool Chunk::isReady()
{
    if (knownReady)
    {
        return true;
    }

    if (loadingInProgress)
    {
        return false;
    }

    std::shared_ptr<WorldGridNode> node = this->node.lock();
    if (node != nullptr)
    {
        if (!node->isReady())
        {
            return false;
        }
    }

    knownReady = true;
    return true;
}

void Chunk::dumpComponents()
{
    for (std::shared_ptr<WorldGridNode> node=getNode(); node!=nullptr; node=node->getParent())
    {
        node->dumpComponents();
    }
}

void Chunk::beginGridGeneration(ChunkWindow &window)
{
    if (encoder != nullptr)
    {
        crash("Sanity check failed: Chunk::beginGridGeneration: encoder already exists.");
    }

    encoder = std::make_shared<ChunkEncoder>(window, *this);
    regenerateGrid();
}

bool Chunk::tryEncodeIntoSVO()
{
    if (encoder == nullptr)
    {
        return true;
    }

    if (!gridGenerated)
    {
        return false;
    }

    if (dirty)
    {
        regenerateGrid();
        return false;
    }

    encoder->encode();
    return true;
}

void Chunk::onBlockUpdated(glm::ivec3 offset)
{
    if (encoder != nullptr && gridGenerated)
    {
        encoder->refreshBlock(base + offset);
    }
    else
    {
        dirty = true;
    }
}

void Chunk::refreshBlock(glm::ivec3 offset)
{
    if (encoder != nullptr && gridGenerated)
    {
        encoder->refreshBlock(base + offset);
    }
}

void Chunk::detach()
{
    if (encoder == nullptr)
    {
        return;
    }

    encoder->detach();
    encoder = nullptr;
}

void Chunk::updateEffects(ChunkWindow &window)
{
    if (encoder == nullptr || !gridGenerated)
    {
        return;
    }
    
    encoder->updateEffects(window);
}

bool Chunk::hasBlockEffects()
{
    if (encoder == nullptr)
    {
        return false;
    }

    return encoder->hasBlockEffects();
}

bool Chunk::isEncoding()
{
    return encoder != nullptr;
}