#include <MachineComponent/ElectricComponent/ElectricComponent.hpp>
#include <Machine/MEngineeringTable/MEngineeringTable.hpp>
#include <Serialize/Base.hpp>
#include <Serialize/Serialize.hpp>
#include <ItemRecipeRegistry/ItemRecipeRegistry.hpp>
#include <Item/IEnergyContainingItem.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <MachineComponent/DataCableComponent/DataCableComponent.hpp>
#include <Block/SpriteBlock/EngineeringTable/EngineeringTable.hpp>

ItemStack& MEngineeringTable::rechargeSlot()
{
    return stacks[6*6];
}

Side MEngineeringTable::getEnergyInputSide()
{
    return Side(*m_world.getBlockMeta(pos) & 3);
}

bool MEngineeringTable::haveEnoughInputItem(ItemStack &inputStack)
{
    // Count this item.
    int amountInInput = 0;
    int i;
    for (i=0; i<6*3; i++)
    {
        ItemStack &stack = stacks[i];
        if (!stack.isEmpty() && inputStack.canMergeFrom(stack))
        {
            amountInInput += stack.getStackSize();
        }
    }

    // If the amount in the input inventory is too low, we can't make.
    if (amountInInput < inputStack.getStackSize())
    {
        return false;
    }

    return true;
}

bool MEngineeringTable::canMakeNext()
{
    // Check that the input inventory contains enough of each input item.
    std::vector<ItemStack>::iterator it;
    for (it=inputStacks.begin(); it!=inputStacks.end(); ++it)
    {
        ItemStack &inputStack = *it;

        // If we don't have enough of this input item, we can't make.
        if (!haveEnoughInputItem(inputStack))
        {
            return false;
        }
    }

    // If there are any empty stacks in the output inventory, we can definitely make.
    int i;
    for (i=6*3; i<6*6; i++)
    {
        if (stacks[i].isEmpty())
        {
            return true;
        }
    }

    // Otherwise, make sure we can merge the output stack into existing stacks.
    int numCanMerge = 0;
    for (i=6*3; i<6*6; i++)
    {
        ItemStack &stack = stacks[i];
        if (stack.canMergeFrom(outputStack))
        {
            numCanMerge += (stack.getMaxStackSize() - stack.getStackSize());
        }
    }

    // We can make as long as we can merge enough.
    return numCanMerge >= outputStack.getStackSize();
}

void MEngineeringTable::makeNext()
{
    // Remove the correct amount of each input stack from the input inventory.
    std::vector<ItemStack>::iterator it;
    for (it=inputStacks.begin(); it!=inputStacks.end(); ++it)
    {
        ItemStack &inputStack = *it;
        int numLeft = inputStack.getStackSize();

        int i;
        for (i=0; i<6*3; i++)
        {
            if (numLeft == 0)
            {
                break;
            }

            ItemStack &stack = stacks[i];
            if (inputStack.canMergeFrom(stack))
            {
                int takingAway = std::min(stack.getStackSize(), numLeft);
                stack.setStackSize(stack.getStackSize() - takingAway);
                numLeft -= takingAway;

                if (stack.getStackSize() == 0) stack = ItemStack();
            }
        }
    }

    // First try to merge as much as possible of the output stack into existing stacks.
    ItemStack pushingStack = outputStack;
    
    int i;
    for (i=6*3; i<6*6; i++)
    {
        if (pushingStack.isEmpty())
        {
            break;
        }

        ItemStack &stack = stacks[i];
        if (!stack.isEmpty() && stack.canMergeFrom(pushingStack))
        {
            int unitsToMerge = std::min(stack.getMaxStackSize() - stack.getStackSize(), pushingStack.getStackSize());
            stack.setStackSize(stack.getStackSize() + unitsToMerge);
            pushingStack.setStackSize(pushingStack.getStackSize() - unitsToMerge);
        }
    }

    // If we have an empty stack then we are done.
    if (pushingStack.isEmpty())
    {
        return;
    }

    // Otherwise, place this stack in an empty slot.
    for (i=6*3; i<6*6; i++)
    {
        ItemStack &stack = stacks[i];
        if (stack.isEmpty())
        {
            stack = pushingStack;
            return;
        }
    }
}

void MEngineeringTable::checkTrigger(DataLink::ChannelBitmap &outputs, Trigger trigger)
{
    auto triggerIter = channelMap.find(trigger);
    if (triggerIter == channelMap.end())
    {
        return;
    }

    if (!isTriggerActive(trigger))
    {
        return;
    }

    outputs += triggerIter->second;
}

bool MEngineeringTable::isTriggerActive(Trigger trigger)
{
    switch (trigger)
    {
    case Trigger::NotEnoughPower:
        return getStoredEnergy() < getPowerUsage();
    }
}

int MEngineeringTable::getPowerUsage()
{
    int totalItems = 0;
    for (ItemStack &inputStack : inputStacks)
    {
        totalItems += inputStack.getStackSize();
    }

    return std::max(1, totalItems / 2);
}

bool MEngineeringTable::cycle()
{
    ElectricComponent &electricComp = getComponent<ElectricComponent>();
    electricComp.rechargeFrom(rechargeSlot(), energyCapacity);

    // If we are switched off, cancel progress and do nothing more.
    if (!isSwitchedOnPriv)
    {
        progress = 0;
        return false;
    }

    // Try to drain energy from the input.
    electricComp.rechargeFrom(getEnergyInputSide(), energyCapacity);

    // If we have no recipe set, reset progress and do nothing more.
    if (outputStack.isEmpty() || inputStacks.size() == 0)
    {
        progress = 0;
        return false;
    }

    // If the recipe cannot be made, reset progress to 0 and do nothing more.
    if (!canMakeNext())
    {
        progress = 0;
        return false;
    }

    // Calculate how much power we need, and pause if not ready yet.
    int powerNeeded = getPowerUsage();

    // Drain the required energy and make progress.
    if (!electricComp.tryConsume(powerNeeded))
    {
        return false;
    }

    progress++;

    // If the process is over, reset progress and make the item.
    if (progress >= completedProgress)
    {
        progress = 0;
        makeNext();
    }

    return true;
}

MEngineeringTable::MEngineeringTable(LocalWorld &world, glm::ivec3 pos) :
    Machine(world, pos),
    stacks(6*6 + 1),
    progress(0),
    isSwitchedOnPriv(false),
    recipeUpdateCounter(0)
{
    attach<ElectricComponent>();
    attach<DataCableComponent>();
}

int MEngineeringTable::getStoredEnergy()
{
    return getComponent<ElectricComponent>().getEnergy();
}

int MEngineeringTable::getProgress()
{
    return progress;
}

bool MEngineeringTable::isSwitchedOn()
{
    return isSwitchedOnPriv;
}

std::vector<ItemStack> MEngineeringTable::getInputStacks()
{
    return inputStacks;
}

ItemStack MEngineeringTable::getOutputStack()
{
    return outputStack;
}

int MEngineeringTable::getRecipeUpdateCounter()
{
    return recipeUpdateCounter;
}

void MEngineeringTable::setSwitch(bool on)
{
    isSwitchedOnPriv = on;
}

void MEngineeringTable::setRecipe(uint32_t id)
{
    ItemRecipe *recipe = itemRecipeRegistry.getRecipeByID(id);
    if (recipe == nullptr)
    {
        return;
    }

    progress = 0;
    inputStacks = recipe->identity.inputStacks;
    outputStack = recipe->identity.outputStack;
    inputItemChannelTable.clear();
    inputItemChannelTable.resize(inputStacks.size());
    recipeUpdateCounter++;
}

std::optional<DataLink::Channel> MEngineeringTable::getTriggerOutputChannel(Trigger trigger)
{
    auto triggerIter = channelMap.find(trigger);
    if (triggerIter == channelMap.end())
    {
        return std::nullopt;
    }

    return triggerIter->second;
}

bool MEngineeringTable::syncChannelTable(ChannelTable &table)
{
    bool anyChanged = false;

    for (size_t i=0; i<table.size(); i++)
    {
        Trigger trigger = static_cast<Trigger>(i);

        std::optional<DataLink::Channel> &valueInTable = table[i];
        std::optional<DataLink::Channel> currentValue = getTriggerOutputChannel(trigger);

        if (valueInTable != currentValue)
        {
            valueInTable = currentValue;
            anyChanged = true;
        }
    }

    return anyChanged;
}

void MEngineeringTable::setTriggerToChannel(Trigger trigger, std::optional<DataLink::Channel> channel)
{
    if (!channel.has_value())
    {
        auto triggerIter = channelMap.find(trigger);
        if (triggerIter != channelMap.end())
        {
            channelMap.erase(triggerIter);
        }
    }
    else
    {
        channelMap[trigger] = *channel;
    }
}

std::optional<DataLink::Channel> MEngineeringTable::getInputItemOutputChannel(size_t index)
{
    return inputItemChannelTable[index];
}

bool MEngineeringTable::syncItemInputChannelTable(ItemInputChannelTable &table)
{
    if (table.size() != inputItemChannelTable.size())
    {
        table = inputItemChannelTable;
        return true;
    }

    bool anyChanged = false;

    size_t i;
    for (i=0; i<table.size(); i++)
    {
        if (table[i] != inputItemChannelTable[i])
        {
            table[i] = inputItemChannelTable[i];
            anyChanged = true;
        }
    }

    return anyChanged;
}

void MEngineeringTable::setInputItemToChannel(int recipeUpdateCounter, size_t index, std::optional<DataLink::Channel> channel)
{
    if (recipeUpdateCounter != this->recipeUpdateCounter)
    {
        return;
    }

    inputItemChannelTable[index] = channel;
}

void MEngineeringTable::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, stacks, progress, isSwitchedOnPriv, inputStacks, outputStack, channelMap, inputItemChannelTable);
}

void MEngineeringTable::deserializeState(const uint8_t *data, size_t size)
{
    deserialize(data, size, stacks, progress, isSwitchedOnPriv, inputStacks, outputStack, channelMap, inputItemChannelTable);
}

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

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

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

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

void MEngineeringTable::update()
{
    uint8_t oldMeta = *m_world.getBlockMeta(pos);
    uint8_t newMeta = oldMeta & 3;

    if (cycle())
    {
        newMeta |= EngineeringTable::metaActive;
    }

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

bool MEngineeringTable::canConnectElectricWire(Side side)
{
    return side == getEnergyInputSide();
}

Side MEngineeringTable::getDataInputSide()
{
    return Side::Bottom;
}

DataLink::ChannelBitmap MEngineeringTable::getActiveChannels()
{
    DataLink::ChannelBitmap outputs;

    checkTrigger(outputs, Trigger::NotEnoughPower);

    for (size_t i=0; i<inputItemChannelTable.size(); i++)
    {
        std::optional<DataLink::Channel> channel = inputItemChannelTable[i];
        if (!channel.has_value())
        {
            continue;
        }

        if (!haveEnoughInputItem(inputStacks[i]))
        {
            outputs += *channel;
        }
    }

    return outputs;
}

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

void MEngineeringTable::onItemStackEntering(Side side, ItemStack &insertingStack)
{
    for (int i=0; i<6*3; i++)
    {
        if (insertingStack.isEmpty())
        {
            return;
        }

        ItemStack &current = stacks[i];
        if (current.isEmpty())
        {
            continue;
        }

        if (current.getStackSize() == current.getMaxStackSize() || !current.canMergeFrom(insertingStack))
        {
            continue;
        }
        
        current.leftMergeFrom(insertingStack);
    }

    // If anything is remaining, put it an empty slot.
    if (insertingStack.isEmpty())
    {
        return;
    }

    for (int i=0; i<6*3; i++)
    {
        ItemStack &current = stacks[i];
        if (current.isEmpty())
        {
            current = insertingStack;
            insertingStack = ItemStack();
            return;
        }
    }
}

ItemStack MEngineeringTable::tryExtractItem(Side side, ItemFilter &filter, uint32_t modeCode)
{
    for (int i=6*3; i<6*6; i++)
    {
        ItemStack &stack = stacks[i];
        if (!stack.isEmpty() && filter.matches(stack))
        {
            return stack.extractOne();
        }
    }

    return ItemStack();
}

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

void MEngineeringTable::getExtractionModes(Side side, std::vector<ExtractionModeSpec> &specs)
{
    specs.push_back({0, "From output slots"});
}

void MEngineeringTable::saveMainConfig(MachineConfig &outConfig)
{
    outConfig.blockName = "main:engineeringTable";
    outConfig.label = "Engineering Table";

    serializeInto(outConfig.components[""], isSwitchedOnPriv, inputStacks, outputStack, channelMap, inputItemChannelTable);
}

void MEngineeringTable::loadMainConfig(const MachineConfig &config, bool consumed)
{
    const auto &data = config.components.at("");
    deserialize(data.data(), data.size(), isSwitchedOnPriv, inputStacks, outputStack, channelMap, inputItemChannelTable);
    recipeUpdateCounter++;
}