#include <Utilities/CircularInt.hpp>
#include <WorldGridNode/WorldGridNode.hpp>
#include <Utilities/Utilities.hpp>
#include <World/World.hpp>
#include <Chunk/Chunk.hpp>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <WorldComponentRegistry/WorldComponentRegistry.hpp>
#include <WorldComponent/WorldComponent.hpp>

WorldGridNode::WorldGridNode(std::shared_ptr<WorldGridNode> parent, glm::ivec3 base, uint32_t parentIndex) :
    parent(parent),
    base(base),
    parentIndex(parentIndex)
{
    // Compute our level.
    if (parent == nullptr)
    {
        level = 7;
    }
    else if (parent->level == 0)
    {
        crash("Attempted to create a child for a level 0 WorldGridNode.");
    }
    else
    {
        level = parent->level - 1;
    }

    // If we are not level 0, then we need a vector of children.
    if (level != 0)
    {
        children.resize(16 * 16 * 16);
    }

    // Calculate our bounding box.
    glm::ivec3 bboxSize(~getLevelBaseMask(level) + 1);

    bbox.x0 = base.x;
    bbox.x1 = base.x + bboxSize.x;
    bbox.y0 = base.y;
    bbox.y1 = base.y + bboxSize.y;
    bbox.z0 = base.z;
    bbox.z1 = base.z + bboxSize.z;
}

std::shared_ptr<WorldGridNode> WorldGridNode::getParent()
{
    return parent.lock();
}

uint16_t WorldGridNode::getChildIndex(glm::ivec3 pos)
{
    return extractIndex(level, pos.x, 0) | extractIndex(level, pos.y, 4) | extractIndex(level, pos.z, 8);
}

std::shared_ptr<WorldGridNode> WorldGridNode::walkDown(World &world, glm::ivec3 pos, bool create)
{
    if (level == 0)
    {
        crash("walkDown() called on a level 0 node!");
    }

    uint16_t index = getChildIndex(pos);
    if (create && children[index] == nullptr)
    {
        std::shared_ptr<WorldGridNode> child = std::make_shared<WorldGridNode>(
            shared_from_this(),
            pos & getLevelBaseMask(level - 1),
            index
        );

        children[index] = child;
        world.onWorldGridNodeCreated(child);
        numChildren++;
        return child;
    }

    return children[index];
}

glm::ivec3 WorldGridNode::getBase()
{
    return base;
}

int WorldGridNode::getLevel()
{
    return level;
}

bool WorldGridNode::containsPos(glm::ivec3 pos)
{
    if (level == 7)
    {
        return true;
    }

    int32_t mask = getLevelBaseMask(level);
    if ((pos & mask) == base)
    {
        return true;
    }

    return false;
}

std::string WorldGridNode::getBaseDir()
{
    return formatString("wgrid/lvl$+/x$+/y$+/z$+", level, base.x, base.y, base.z);
}

void WorldGridNode::retainChunk(World &world, float relativeDeadline)
{
    if (chunk == nullptr)
    {
        chunk = std::make_shared<Chunk>(shared_from_this(), base);
        world.beginChunkLoad(chunk, relativeDeadline);
    }

    chunk->retainCount++;
}

void WorldGridNode::unretainChunk(World &world)
{
    if (chunk == nullptr)
    {
        return;
    }

    if (--chunk->retainCount == 0)
    {
        world.enqueueForPurge(chunk);
    }
}

void WorldGridNode::updateChunkCountsWithinBox(
    World &world,
    const BoundingBox<cint32> &box,
    int delta,
    const std::function<void(Chunk&)> &onHit
)
{
    if (level == 0)
    {
        if (delta == 1)
        {
            glm::ivec3 middle = glm::ivec3(
                box.x0 + (box.x1 - box.x0) / 2,
                box.y0 + (box.y1 - box.y0) / 2,
                box.z0 + (box.z1 - box.z0) / 2
            );

            double distance = glm::length(glm::dvec3(middle - base));
            retainChunk(world, (float) distance / (32.0 * 16.0));
        }
        else if (delta == -1)
        {
            unretainChunk(world);
        }
        else
        {
            crash("Invalid delta passed to updateChunkCountsWithinBox()");
        }

        if (chunk != nullptr)
        {
            onHit(*chunk);
        }

        return;
    }

    BoundingBox<cint32> intersection = this->bbox & box;
    if (level == 7)
    {
        intersection = box;
    }

    if (intersection.getVolume() == 0)
    {
        return;
    }

    cint32 childBaseMask = getLevelBaseMask(level - 1);
    cint32 nodeSize = (~childBaseMask) + 1;

    wvec3 pos = wvec3(0);
    for (pos.z=(intersection.z0 & childBaseMask); pos.z!=((intersection.z1 + ~childBaseMask) & childBaseMask); pos.z+=nodeSize)
    {
        for (pos.y=(intersection.y0 & childBaseMask); pos.y!=((intersection.y1 + ~childBaseMask) & childBaseMask); pos.y+=nodeSize)
        {
            for (pos.x=(intersection.x0 & childBaseMask); pos.x!=((intersection.x1 + ~childBaseMask) & childBaseMask); pos.x+=nodeSize)
            {
                std::shared_ptr<WorldGridNode> child = walkDown(world, pos, true);
                child->updateChunkCountsWithinBox(world, box, delta, onHit);
            }
        }
    }
}

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

    if (loadingInProgress)
    {
        return false;
    }

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

    knownReady = true;
    return true;
}

void WorldGridNode::markLoaded()
{
    loadingInProgress = false;
}

void WorldGridNode::loadComponents(AtomicDataStore &manager)
{
    std::string baseDir = getBaseDir();
    worldComponentRegistry.loadComponents(manager, baseDir, components);
}

void WorldGridNode::updateComponents(LocalWorld &world, DataUpdatePlanner &planner)
{
    for (const std::unique_ptr<WorldComponent> &comp : components)
    {
        if (comp->update(world, shared_from_this()))
        {
            comp->serializeState(planner.newFile(comp->dataFilePath));
        }
    }
}

size_t WorldGridNode::getParentIndex()
{
    return parentIndex;
}

bool WorldGridNode::isRoot()
{
    return level == 7;
}

void WorldGridNode::dumpComponents()
{
    for (const std::unique_ptr<WorldComponent> &comp : components)
    {
        comp->dump(shared_from_this());
    }
}

void WorldGridNode::purge()
{
    if (numChildren != 0)
    {
        return;
    }

    std::shared_ptr<WorldGridNode> parent = getParent();
    if (parent == nullptr)
    {
        return;
    }

    parent->children[parentIndex] = nullptr;
    parent->numChildren--;
    parent->purge();
}