#include <Block/Block.hpp>
#include <ItemStack/ItemStack.hpp>
#include <Machine/MItemProcessor/MItemProcessor.hpp>
#include <Serialize/Serialize.hpp>
#include <Item/IEnergyContainingItem.hpp>
#include <Machine/IElectricMachine.hpp>
#include <Machine/IDataMachine.hpp>
#include <Item/StatelessItem/BasicItem/MachineUpgrade/MachineUpgrade.hpp>
#include <MachineComponent/ElectricComponent/ElectricComponent.hpp>
#include <MachineComponent/DataCableComponent/DataCableComponent.hpp>
#include <MachineComponent/MultiBlockComponent/MultiBlockComponent.hpp>
#include <MachineComponent/BeltComponent/BeltComponent.hpp>

ItemStack& MItemProcessor::inputSlot()
{
    return stacks[0];
}

ItemStack& MItemProcessor::outputSlot()
{
    return stacks[1];
}

ItemStack& MItemProcessor::rechargeSlot()
{
    return stacks[2];
}

bool MItemProcessor::cycle()
{
    ElectricComponent &electricComp = getComponent<ElectricComponent>();
    MultiBlockComponent &multiBlockComp = getComponent<MultiBlockComponent>();

    int efficiency, speed;
    calculateStats(efficiency, speed);

    electricComp.rechargeFrom(rechargeSlot(), energyCapacity);
    electricComp.rechargeFrom(multiBlockComp.getEnergyInput(), energyCapacity);

    // If there is no input, abort progress.
    if (inputSlot().isEmpty())
    {
        progress = 0;
        return false;
    }

    // If the input cannot be processed, abort progress.
    ItemStack output = getOutputItem(inputSlot());
    if (output.isEmpty())
    {
        progress = 0;
        return false;
    }

    // If the output slot is not empty and also can't merge the result, reset progress to zero.
    if (!outputSlot().isEmpty() && !outputSlot().canMergeFrom(output))
    {
        progress = 0;
        return false;
    }

    // Same if the stack is too big.
    if (!outputSlot().isEmpty() && outputSlot().getStackSize() > outputSlot().getMaxStackSize() - output.getStackSize())
    {
        progress = 0;
        return false;
    }

    // Calculate the effective power usage.
    int effectivePowerUsage = getEffectivePowerUsage(efficiency, speed);

    // If we don't have power, pause progress.
    if (!electricComp.tryConsume(effectivePowerUsage))
    {
        return false;
    }

    // Make progress.
    progress += speed;
    if (progress >= getCompletedProgress())
    {
        progress -= getCompletedProgress();
        outputSlot().leftMergeFrom(output);
        inputSlot().extractOne();
    }

    return true;
}

bool MItemProcessor::isTriggerActive(Trigger trigger)
{
    int efficiency, speed;

    switch (trigger)
    {
    case Trigger::InputSlotEmpty:
        return inputSlot().isEmpty();
    case Trigger::NotEnoughPower:
        calculateStats(efficiency, speed);
        return getStoredEnergy() < getEffectivePowerUsage(efficiency, speed);
    }
}

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

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

    outputs += triggerIter->second;
}

int MItemProcessor::getEffectivePowerUsage(int efficiency, int speed)
{
    int effectivePowerUsage = getPowerUsage() * speed;
    int powerSaved = efficiency * effectivePowerUsage / 66;
    effectivePowerUsage -= powerSaved;

    return effectivePowerUsage;
}

void MItemProcessor::calculateStats(int &efficiency, int &speed)
{
    speed = 1;
    efficiency = 0;

    for (int i=3; i<7; i++)
    {
        MachineUpgrade *upgrade = stacks[i].getItemAs<MachineUpgrade>();
        if (upgrade != nullptr)
        {
            speed += upgrade->speedDelta * stacks[i].getStackSize();
            efficiency += upgrade->efficiencyDelta * stacks[i].getStackSize();
        }
    }
}

MItemProcessor::MItemProcessor(LocalWorld &world, glm::ivec3 pos) :
    Machine(world, pos),
    stacks(7),
    progress(0)
{
    attach<ElectricComponent>();
    attach<MultiBlockComponent>();
    attach<DataCableComponent>();
    attach<BeltComponent>();
}

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

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

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

    return triggerIter->second;
}

bool MItemProcessor::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 MItemProcessor::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;
    }
}

void MItemProcessor::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, stacks, progress, channelMap);
}

void MItemProcessor::deserializeState(const uint8_t *data, size_t size)
{
    deserialize(data, size, stacks, progress, channelMap);
}

void MItemProcessor::saveMainConfig(MachineConfig &outConfig)
{
    Block *block = m_world.getBlockAt(pos);
    outConfig.blockName = block->name;
    outConfig.label = block->displayName;

    std::vector<ItemStack> upgrades;

    for (int i=3; i<7; i++)
    {
        ItemStack &upgradeStack = stacks[i];
        if (upgradeStack.isEmpty())
        {
            continue;
        }

        outConfig.consumedItems.push_back(upgradeStack);
        upgrades.push_back(upgradeStack);
    }

    serializeInto(outConfig.components[""], progress, channelMap, upgrades);
}

void MItemProcessor::loadMainConfig(const MachineConfig &config, bool consumed)
{
    std::vector<ItemStack> upgrades;

    const auto &data = config.components.at("");
    deserialize(data.data(), data.size(), progress, channelMap, upgrades);

    if (consumed)
    {
        for (int i = 0; i < upgrades.size() && i < 4; i++)
        {
            stacks[i + 3] = upgrades[i];
        }
    }
}

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

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

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

    if (slot == 0)
    {
        // Input slot updated, cancel smelting progress.
        progress = 0;
    }
}

bool MItemProcessor::isAllowedStackInSlot(int slot, ItemStack stack)
{
    if (stack.isEmpty())
    {
        // All slots can be emptied.
        return true;
    }

    if (slot == 0)
    {
        // You can place anything you want into the input slot.
        return true;
    }
    else if (slot == 1)
    {
        // Cannot place anything in the output slot.
        return false;
    }
    else if (slot == 2)
    {
        // Energy containers in the recharge slot.
        return stack.getItemAs<IEnergyContainingItem>() != nullptr;
    }
    else
    {
        // Upgrade slot; make sure this item is an upgrade.
        MachineUpgrade *upgrade = stack.getItemAs<MachineUpgrade>();
        if (upgrade == nullptr)
        {
            return false;
        }

        // Make sure this upgrade isn't already in another slot.
        int i;
        for (i=3; i<7; i++)
        {
            if (i == slot)
            {
                continue;
            }

            if (!stacks[i].isEmpty() && stacks[i].getItem()->name == stack.getItem()->name)
            {
                return false;
            }
        }

        // Rules met, allow in.
        return true;
    }
}

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

    if (cycle())
    {
        newMeta |= (1 << 2);
    }

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

bool MItemProcessor::canConnectElectricWire(Side side)
{
    return false;
}

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

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

    checkTrigger(outputs, Trigger::InputSlotEmpty);
    checkTrigger(outputs, Trigger::NotEnoughPower);

    return outputs;
}

void MItemProcessor::onItemStackEntering(Side side, ItemStack &stack)
{
    inputSlot().insertFrom(stack);
}

ItemStack MItemProcessor::tryExtractItem(Side side, ItemFilter &filter, uint32_t modeCode)
{
    if (!filter.matches(outputSlot()))
    {
        return ItemStack();
    }

    return outputSlot().extractOne();
}

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

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