#include <BlockRegistry/BlockRegistry.hpp>
#include <Item/Item.hpp>
#include <Machine/MItemExtractor/MItemExtractor.hpp>
#include <MachineComponent/DataCableComponent/DataCableComponent.hpp>
#include <MachineComponent/ElectricComponent/ElectricComponent.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <Block/SpriteBlock/ItemExtractor/ItemExtractor.hpp>
#include <Block/SpriteBlock/ConveyorBelt/ConveyorBelt.hpp>
#include <MachineComponent/BeltComponent/BeltComponent.hpp>
#include <Belt/Belt.hpp>
#include <ItemFilter/CompleteItemFilter/CompleteItemFilter.hpp>
#include <ItemFilter/ItemFilterByStack/ItemFilterByStack.hpp>
#include <ItemFilter/InvertedItemFilter/InvertedItemFilter.hpp>
#include <MachineComponent/BeltMotorComponent/BeltMotorComponent.hpp>
#include <memory>

std::unique_ptr<ItemFilter> MItemExtractor::computeFilter()
{
    if (filterType == FilterType::All)
    {
        return std::make_unique<CompleteItemFilter>();
    }

    std::unique_ptr<ItemFilter> result = std::make_unique<ItemFilterByStack>(filterStacks);
    if (filterType == FilterType::AllExcept)
    {
        result = std::make_unique<InvertedItemFilter>(std::move(result));
    }

    return result;
}

void MItemExtractor::markFilterUpdated()
{
    filterUpdateCounter++;
    currentFilter = computeFilter();
}

MItemExtractor::MItemExtractor(LocalWorld &world, glm::ivec3 pos) : Machine(world, pos)
{
    attach<ElectricComponent>();
    attach<BeltComponent>();
    attach<DataCableComponent>();
    attach<BeltMotorComponent>();
}

bool MItemExtractor::isSwitchedOn()
{
    return switchedOn;
}

FilterType MItemExtractor::getFilterType()
{
    return filterType;
}

void MItemExtractor::setSwitch(bool on)
{
    switchedOn = on;
}

void MItemExtractor::setFilterType(FilterType type)
{
    filterType = type;
    currentFilter = computeFilter();
}

bool MItemExtractor::areModeSpecsLoaded()
{
    return modeSpecsLoaded;
}

const std::vector<IBeltMachine::ExtractionModeSpec>& MItemExtractor::getModeSpecs()
{
    return modeSpecs;
}

uint32_t MItemExtractor::getModeCode()
{
    return currentModeCode;
}

void MItemExtractor::setModeCode(uint32_t modeCode)
{
    currentModeCode = modeCode;
}

void MItemExtractor::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, switchedOn, filterType, filterStacks, currentModeCode);
}

void MItemExtractor::deserializeState(const uint8_t *data, size_t size)
{
    deserialize(data, size, switchedOn, filterType, filterStacks, currentModeCode);
}

void MItemExtractor::saveMainConfig(MachineConfig &outConfig)
{
    outConfig.blockName = "main:itemExtractor";
    outConfig.label = "Item Extractor";

    serializeInto(outConfig.components[""], switchedOn, filterType, filterStacks, currentModeCode);
}

void MItemExtractor::loadMainConfig(const MachineConfig &config, bool consumed)
{
    const auto &data = config.components.at("");
    deserialize(data.data(), data.size(), switchedOn, filterType, filterStacks, currentModeCode);
}

bool MItemExtractor::canConnectElectricWire(Side side)
{
    return side == Side::Bottom;
}

void MItemExtractor::update()
{
    if (currentFilter == nullptr)
    {
        currentFilter = computeFilter();
    }

    uint8_t oldMeta = *m_world.getBlockMeta(pos);
    uint8_t newMeta = oldMeta & 3;

    Side outputSide = ItemExtractor::getMetaOutputSide(oldMeta);
    Side inputSide = outputSide.opposite();

    BeltComponent &beltComp = getComponent<BeltComponent>();
    std::shared_ptr<Belt> belt = beltComp.getOutputBelt(outputSide);
    ElectricComponent &electricComp = getComponent<ElectricComponent>();

    glm::ivec3 inputPos = pos + inputSide.out();
    std::shared_ptr<Machine> machine;

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

        finishProtectorSearch:;
    }
    else
    {
        machine = m_world.getMachine(inputPos);
    }

    std::shared_ptr<IBeltMachine> inputMachine = std::dynamic_pointer_cast<IBeltMachine>(machine);

    if (!modeSpecsLoaded && inputMachine != nullptr)
    {
        modeSpecsLoaded = true;
        inputMachine->getExtractionModes(inputSide.opposite(), modeSpecs);
    }

    if (switchedOn)
    {
        if (!belt->hasItems() || static_cast<int>(belt->elements.front().abstractPos + belt->beltOffset) >= itemSpacing + itemInitialOffset)
        {
            if (inputMachine != nullptr)
            {
                electricComp.rechargeFrom(Side::Bottom, energyPerItem);
                if (electricComp.getEnergy() >= energyPerItem)
                {
                    ItemStack stack = inputMachine->tryExtractItem(inputSide.opposite(), *currentFilter, currentModeCode);
                    if (!stack.isEmpty())
                    {
                        electricComp.drainEnergy(energyPerItem);
                        belt->elements.push_front(
                            Belt::Element{-belt->beltOffset + itemInitialOffset, stack}
                        );

                        belt->elementUpdateNum++;
                    }
                }
            }
        }
    }

    if (getComponent<BeltMotorComponent>().tryAdvance(belt))
    {
        newMeta |= ItemExtractor::metaActive;
    }

    if (newMeta != oldMeta)
    {
        m_world.setBlockMeta(pos, newMeta);
    }
}

void MItemExtractor::onDataInputVerb(std::string verb)
{
    if (verb == Verbs::TurnOn)
    {
        switchedOn = true;
    }
    else if (verb == Verbs::TurnOff)
    {
        switchedOn = false;
    }
    else
    {
        Log::warn(formatString("MItemExtractor::onDataInputVerb(): received an unknown verb '$+'", verb));
    }
}

Side MItemExtractor::getDataInputSide()
{
    return Side::Top;
}

int MItemExtractor::getFilterUpdateCounter()
{
    return filterUpdateCounter;
}

std::vector<ItemStack> MItemExtractor::getFilterItemStacks()
{
    return filterStacks;
}

void MItemExtractor::addItemFilter(ItemStack stack)
{
    filterStacks.push_back(stack);
    markFilterUpdated();
}

void MItemExtractor::removeItemFilter(int index)
{
    if (index < 0 || index >= (int) filterStacks.size())
    {
        return;
    }

    filterStacks.erase(filterStacks.begin() + index);
    markFilterUpdated();
}