#include <memory>

#include <BeltManager/BeltManager.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <Utilities/Utilities.hpp>
#include <Block/Block.hpp>
#include <Entity/PhysicsEntity/ItemEntity/ItemEntity.hpp>
#include <ItemStack/ItemStack.hpp>
#include <Machine/IBeltMachine.hpp>
#include <Machine/Machine.hpp>

#define BELT_FILE_PATH "belts.dat"

BeltManager::BeltManager(LocalWorld &world) : world(world) {}

void BeltManager::load()
{
    std::vector<uint8_t> data;
    world.planner.read(BELT_FILE_PATH, world.dataStore, data);
    deserialize(data.data(), data.size(), belts);

    for (std::shared_ptr<Belt> &belt : belts)
    {
        belt->id = world.allocBeltId();
        world.onBeltUpdated(*belt, false);
    }
}

void BeltManager::save()
{
    serializeInto(world.planner.newFile(BELT_FILE_PATH), belts);
}

std::shared_ptr<Belt> BeltManager::acquireOperationalBelt(glm::ivec3 sourcePos, Side dir)
{
    for (const std::shared_ptr<Belt> &belt : belts)
    {
        if (belt->startPos == sourcePos && belt->dir == dir)
        {
            if (!belt->isOperational)
            {
                crash(
                    "acquireOperationalBelt was called with a source position and direction of "
                    "an existing belt, but the belt is not operational. This implies that a machine "
                    "is being created over an existing belt, which is an inconsistent state."
                );
            }

            return belt;
        }
    }

    for (std::shared_ptr<Belt> &belt : belts)
    {
        if (!belt->isOperational && belt->dir == dir && belt->startPos + belt->dir.in() == sourcePos)
        {
            belt->startPos += belt->dir.in();
            belt->beltBlockLength++;
            belt->isOperational = true;
            belt->beltOffset += 8;
            return belt;
        }
    }

    std::shared_ptr<Belt> newBelt = std::make_shared<Belt>(Belt{world.allocBeltId(), sourcePos, dir, true});
    belts.push_back(newBelt);
    world.onBeltUpdated(*newBelt, false);
    return newBelt;
}

void BeltManager::addBeltSegment(glm::ivec3 pos, Side dir)
{
    // First check if there is a belt that we are extending forward.
    std::shared_ptr<Belt> beltBehind;
    for (size_t i=0; i<belts.size(); i++)
    {
        std::shared_ptr<Belt> &belt = belts[i];
        if (belt->dir == dir && belt->getEndPos() == pos)
        {
            belt->beltBlockLength++;
            beltBehind = belt;
            break;
        }
    }

    // Now check if we are extending one backwards, or merging (only non-operations belts can be
    // extended backwards).
    glm::ivec3 frontStartPos = pos + dir.out();
    for (size_t i=0; i<belts.size(); i++)
    {
        std::shared_ptr<Belt> &belt = belts[i];
        if (!belt->isOperational && belt->dir == dir && belt->startPos == frontStartPos)
        {
            if (beltBehind != nullptr)
            {
                for (const Belt::Element &element : belt->elements)
                {
                    Belt::Element newElement = element;
                    newElement.abstractPos += belt->beltOffset;
                    newElement.abstractPos -= beltBehind->beltOffset;
                    newElement.abstractPos += beltBehind->beltBlockLength * 8;
                    beltBehind->elements.push_back(newElement);
                }

                beltBehind->beltBlockLength += belt->beltBlockLength;
                beltBehind->elementUpdateNum++;

                if (!beltBehind->isOperational)
                {
                    world.onBeltUpdated(*beltBehind, false);
                }

                world.onBeltRemoved(belt->id);

                if (i != belts.size() - 1)
                {
                    std::swap(belt, belts.back());
                }

                belts.resize(belts.size() - 1);
            }
            else
            {
                belt->startPos += dir.in();
                belt->beltBlockLength++;
                belt->beltOffset += 8;

                world.onBeltUpdated(*belt, false);
            }

            return;
        }
    }

    // No belt was found in front of us; if there was a belt behind us which we already merged with,
    // then we are done.
    if (beltBehind != nullptr)
    {
        if (!beltBehind->isOperational)
        {
            world.onBeltUpdated(*beltBehind, false);
        }

        return;
    }

    // Otherwise, we need to create a non-operational belt.
    std::shared_ptr<Belt> newBelt = std::make_shared<Belt>(Belt{world.allocBeltId(), pos, dir, false});
    belts.push_back(newBelt);
    world.onBeltUpdated(*newBelt, false);
}

void BeltManager::removeBeltSegment(glm::ivec3 pos)
{
    for (size_t i=0; i<belts.size(); i++)
    {
        std::shared_ptr<Belt> &belt = belts[i];
        std::optional<int> offsetOpt = belt->getBlockOffset(pos);

        if (!offsetOpt.has_value())
        {
            continue;
        }

        int offset = *offsetOpt;
        if (offset == 0)
        {
            belt->beltBlockLength--;
            belt->isOperational = false;

            for (;;)
            {
                if (belt->elements.size() == 0)
                {
                    break;
                }

                Belt::Element &firstElement = belt->elements.front();
                int voxelOffset = static_cast<int>(firstElement.abstractPos + belt->beltOffset);

                if (voxelOffset >= 8)
                {
                    break;
                }

                world.spawn<ItemEntity>(
                    glm::dvec3(belt->startPos) + glm::dvec3(0.5),
                    glm::vec3(0.0f),
                    firstElement.itemStack
                );

                belt->elements.pop_front();
            }

            belt->startPos += belt->dir.out();
            belt->beltOffset -= 8;

            if (belt->beltBlockLength == 0)
            {
                world.onBeltRemoved(belt->id);

                if (i != belts.size() - 1)
                {
                    std::swap(belt, belts.back());
                }

                belts.resize(belts.size() - 1);
                i--;
            }
            else
            {
                world.onBeltUpdated(*belt, false);
            }
        }
        else if (offset == belt->beltBlockLength - 1)
        {
            belt->beltBlockLength--;

            for (;;)
            {
                if (belt->elements.size() == 0)
                {
                    break;
                }

                Belt::Element &lastElement = belt->elements.back();
                int voxelOffset = static_cast<int>(lastElement.abstractPos + belt->beltOffset);

                if (voxelOffset < 8 * belt->beltBlockLength)
                {
                    break;
                }

                glm::ivec3 pos = belt->startPos + (int) belt->beltBlockLength * belt->dir.out();
                world.spawn<ItemEntity>(
                    glm::dvec3(pos) + glm::dvec3(0.5),
                    glm::vec3(0.0f),
                    lastElement.itemStack
                );

                belt->elements.pop_back();
            }

            if (belt->beltBlockLength == 0)
            {
                world.onBeltRemoved(belt->id);

                if (i != belts.size() - 1)
                {
                    std::swap(belt, belts.back());
                }

                belts.resize(belts.size() - 1);
                i--;
            }
            else if (!belt->isOperational)
            {
                world.onBeltUpdated(*belt, false);
            }
        }
        else
        {
            glm::ivec3 newStartPos = belt->startPos + (offset + 1) * belt->dir.out();
            std::shared_ptr<Belt> newBelt = std::make_shared<Belt>(Belt{world.allocBeltId(), newStartPos, belt->dir, false});
            newBelt->beltBlockLength = belt->beltBlockLength - offset - 1;
            newBelt->beltOffset = belt->beltOffset;

            belt->beltBlockLength = offset;

            auto element = belt->elements.begin();
            while (element != belt->elements.end())
            {
                int voxelOffset = static_cast<int>(element->abstractPos + belt->beltOffset);
                if (voxelOffset >= 8 * belt->beltBlockLength + 8)
                {
                    Belt::Element newElement = *element;
                    newElement.abstractPos -= 8 * belt->beltBlockLength + 8;
                    newBelt->elements.push_back(newElement);
                    element = belt->elements.erase(element);
                }
                else if (voxelOffset >= 8 * belt->beltBlockLength)
                {
                    glm::ivec3 pos = belt->startPos + (int) belt->beltBlockLength * belt->dir.out();
                    world.spawn<ItemEntity>(
                        glm::dvec3(pos) + glm::dvec3(0.5),
                        glm::vec3(0.0f),
                        element->itemStack
                    );

                    element = belt->elements.erase(element);
                }
                else
                {
                    element++;
                }
            }

            if (!belt->isOperational)
            {
                world.onBeltUpdated(*belt, false);
            }

            belts.push_back(newBelt);
            world.onBeltUpdated(*newBelt, false);
        }
    }
}

void BeltManager::tryInsertInto(glm::ivec3 pos, Side side, ItemStack &stack)
{
    if (stack.isEmpty())
    {
        return;
    }

    std::shared_ptr<Machine> machine;

    Block *block = world.getBlockAt(pos);
    if (block != nullptr && block->isSuffixed("Casing"))
    {
        glm::ivec3 protectorPos;
        for (protectorPos.z=pos.z-7; protectorPos.z<=pos.z+7; protectorPos.z++)
        {
            for (protectorPos.y=pos.y-7; protectorPos.y<=pos.y+7; protectorPos.y++)
            {
                for (protectorPos.x=pos.x-7; protectorPos.x<=pos.x+7; protectorPos.x++)
                {
                    Block *protector = world.getBlockAt(protectorPos);
                    if (protector != nullptr && protector->isProtectorOf(world, protectorPos, pos))
                    {
                        machine = world.getMachine(protectorPos);
                        goto finishProtectorSearch;
                    }
                }
            }
        }

        finishProtectorSearch:;
    }
    else
    {
        machine = world.getMachine(pos);
    }

    if (machine != nullptr)
    {
        std::shared_ptr<IBeltMachine> beltMachine = std::dynamic_pointer_cast<IBeltMachine>(machine);
        if (beltMachine != nullptr)
        {
            beltMachine->onItemStackEntering(side, stack);
        }

        return;
    }

    for (std::shared_ptr<Belt> &belt : belts)
    {
        std::optional<int> blockOffset = belt->getBlockOffset(pos);
        if (blockOffset.has_value())
        {
            int voxelOffset = (*blockOffset) * 8 + 2;
            belt->tryInsertAt(voxelOffset, stack);
            
            if (!belt->isOperational)
            {
                world.onBeltUpdated(*belt, false);
            }

            return;
        }
    }

    BoundingBox<wfixed> bbox{
        (wfixed) pos.x, (wfixed) pos.y, (wfixed) pos.z,
        (wfixed) pos.x + wfixed(1.0), (wfixed) pos.y + wfixed(1.0), (wfixed) pos.z + wfixed(1.0)
    };

    block = world.getBlockAt(pos);
    if (block == nullptr || !block->isIntersectedBy(world, pos, bbox))
    {
        world.spawn<ItemEntity>(
            glm::dvec3(pos) + glm::dvec3(0.5),
            glm::vec3(0.0f),
            stack
        );

        stack = ItemStack();
    }
}

void BeltManager::tryAdvanceBelt(glm::ivec3 pos)
{
    for (std::shared_ptr<Belt> &belt : belts)
    {
        std::optional<int> blockOffset = belt->getBlockOffset(pos);
        if (blockOffset.has_value())
        {
            belt->advance(world);
            
            if (!belt->isOperational)
            {
                world.onBeltUpdated(*belt, false);
            }

            return;
        }
    }
}