#include <BoundingBox/BoundingBox.hpp>
#include <Entity/MiningRigEntity/MiningRigEntity.hpp>
#include <Machine/MMiningRig/MMiningRig.hpp>
#include <MachineComponent/BeltComponent/BeltComponent.hpp>
#include <MachineComponent/ElectricComponent/ElectricComponent.hpp>
#include <MachineComponent/MultiBlockComponent/MultiBlockComponent.hpp>
#include <Serialize/Serialize.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <Block/Block.hpp>

void MMiningRig::checkMineQueue()
{
    ElectricComponent &electricComp = getComponent<ElectricComponent>();
    MultiBlockComponent &multiBlockComp = getComponent<MultiBlockComponent>();

    for (ItemStack &stack : mineQueue)
    {
        int drillLength = -drillOffset.z;
        int powerRequired = basePullUpPowerUsage + drillLength / 5;

        if (!stack.isEmpty())
        {
            electricComp.rechargeFrom(multiBlockComp.getEnergyInput(), powerRequired);
            if (!electricComp.tryConsume(powerRequired))
            {
                return;
            }

            insertStack(stack);
        }

        if (!stack.isEmpty())
        {
            electricComp.addEnergy(powerRequired);
            return;
        }
    }

    mineQueue.clear();
}

MMiningRig::MMiningRig(LocalWorld &world, glm::ivec3 pos) :
    Machine(world, pos),
    stacks(30)
{
    attach<BeltComponent>();
    attach<MultiBlockComponent>();
    attach<ElectricComponent>();

    drillOffset = glm::ivec3(0);
}

glm::ivec3 MMiningRig::getDrillPos()
{
    MultiBlockComponent &multiBlockComp = getComponent<MultiBlockComponent>();

    glm::ivec3 tweakedOffset = drillOffset;
    if (drillOffset.y & 1)
    {
        tweakedOffset.x = 4 - tweakedOffset.x;
    }

    if (drillOffset.z & 1)
    {
        tweakedOffset.y = 4 - tweakedOffset.y;
        tweakedOffset.x = 4 - tweakedOffset.x;
    }

    return pos + multiBlockComp.getCasingRelativePos(tweakedOffset + glm::ivec3(1, 1, -1));
}

BoundingBox<int> MMiningRig::getDrillAreaBB()
{
    MultiBlockComponent &multiBlockComp = getComponent<MultiBlockComponent>();

    glm::ivec3 cornerA = pos + multiBlockComp.getCasingRelativePos(glm::ivec3(1, 1, 4));
    glm::ivec3 cornerB = pos + multiBlockComp.getCasingRelativePos(glm::ivec3(5, 5, 4));

    return BoundingBox<int>{
        std::min(cornerA.x, cornerB.x),
        std::min(cornerA.y, cornerB.y),
        std::min(cornerA.z, cornerB.z),

        std::max(cornerA.x, cornerB.x) + 1,
        std::max(cornerA.y, cornerB.y) + 1,
        std::max(cornerA.z, cornerB.z) + 1,
    };
}

void MMiningRig::serializeState(std::vector<uint8_t> &outBytes)
{
    std::vector<uint8_t> statsBytes;
    serializeInto(statsBytes, stats);

    serializeInto(outBytes, stacks, drillOffset, mineQueue, pipesLeft, numLasers, currentMiningDamage, statsBytes);
}

void MMiningRig::deserializeState(const uint8_t *data, size_t size)
{
    std::vector<uint8_t> statsBytes;
    deserialize(data, size, stacks, drillOffset, mineQueue, pipesLeft, numLasers, currentMiningDamage, statsBytes);

    deserialize(statsBytes.data(), statsBytes.size(), stats);
}

int MMiningRig::getNumSlots()
{
    return (int) stacks.size();
}

ItemStack MMiningRig::getStackInSlot(int slot)
{
    return stacks[slot];
}

void MMiningRig::setStackInSlot(int slot, ItemStack stack)
{
    stacks[slot] = stack;
}

bool MMiningRig::isAllowedStackInSlot(int slot, ItemStack stack)
{
    return true;
}

void MMiningRig::onItemStackEntering(Side side, ItemStack &stack)
{
    insertStack(stack);
}

ItemStack MMiningRig::tryExtractItem(Side side, ItemFilter &filter, uint32_t modeCode)
{
    for (size_t i = 0; i < stacks.size(); i++)
    {
        ItemStack &stack = stacks[i];
        if (!stack.isEmpty() && filter.matches(stack))
        {
            return stack.extractOne();
        }
    }

    return ItemStack();
}

bool MMiningRig::canConnectExtractor(Side side)
{
    return true;
}

void MMiningRig::update()
{
    ElectricComponent &electricComp = getComponent<ElectricComponent>();
    MultiBlockComponent &multiBlockComp = getComponent<MultiBlockComponent>();

    stats.depth = -drillOffset.z;

    if (rigEntity == nullptr)
    {
        rigEntity = m_world.spawn<MiningRigEntity>(pos);
    }

    if (numLasers < maxLasers)
    {
        for (ItemStack &stack : stacks)
        {
            if (stack.isEmpty())
            {
                continue;
            }

            if (stack.getItem()->name == "main:laser")
            {
                stack.extractOne();
                numLasers++;
                break;
            }
        }
    }

    if (pipesLeft == 0)
    {
        for (ItemStack &stack : stacks)
        {
            if (stack.isEmpty())
            {
                continue;
            }

            if (stack.getItem()->name == "main:steelIngot")
            {
                stack.extractOne();
                pipesLeft += pipesPerSteelIngot;
                break;
            }
        }
    }

    if (pipesLeft == 0 && drillOffset.x == 4 && drillOffset.y == 4)
    {
        // No pipes and we are on the final block of this layer.
        return;
    }

    glm::ivec3 drillPos = getDrillPos();
    Block *block = m_world.getBlockAt(drillPos);

    if (block == nullptr)
    {
        // We must wait for the chunk to load.
        return;
    }

    checkMineQueue();
    if (mineQueue.size() != 0)
    {
        return;
    }

    int hardness = (int) std::ceil(block->hardness);
    if (currentMiningDamage < hardness)
    {
        int powerUsage = powerInputPerLaser * numLasers;
        electricComp.rechargeFrom(multiBlockComp.getEnergyInput(), powerUsage);
        if (!electricComp.tryConsume(powerUsage))
        {
            return;
        }
    }

    if (currentMiningDamage < hardness)
    {
        currentMiningDamage += miningPowerPerLaser * numLasers;
    }

    if (currentMiningDamage < hardness)
    {
        // We tried charging the laser but we can't dig the block yet.
        return;
    }

    currentMiningDamage -= hardness;

    std::vector<ItemStack> harvestedStacks;
    block->getHarvestedStacks(m_world, drillPos, harvestedStacks);
    m_world.setBlockAt(drillPos, 0);
    stats.blocksMined++;

    for (auto &stack : harvestedStacks)
    {
        while (!stack.isEmpty())
        {
            mineQueue.push_back(stack.extractOne());
        }
    }
    
    if (drillOffset.x == 4)
    {
        if (drillOffset.y == 4)
        {
            pipesLeft--;
            drillOffset.z--;
            drillOffset.x = drillOffset.y = 0;
        }
        else
        {
            drillOffset.x = 0;
            drillOffset.y++;
        }
    }
    else
    {
        drillOffset.x++;
    }

    checkMineQueue();
}

bool MMiningRig::canConnectElectricWire(Side side)
{
    return true;
}