#include <BlockRegistry/BlockRegistry.hpp>
#include <DataLink/DataLink.hpp>
#include <Item/Item.hpp>
#include <Machine/MBeltJunction/MBeltJunction.hpp>
#include <MachineComponent/DataCableComponent/DataCableComponent.hpp>
#include <MachineComponent/ElectricComponent/ElectricComponent.hpp>
#include <Serialize/Serialize.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <Block/SpriteBlock/BeltJunction/BeltJunction.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 <memory>

Side MBeltJunction::getInputSide()
{
    return Side(*m_world.getBlockMeta(pos));
}

MBeltJunction::OutputBelt MBeltJunction::getOutputBelt(ItemStack &stack)
{
    DataCableComponent &dataComp = getComponent<DataCableComponent>();
    DataLink::ChannelBitmap inputChannels = dataComp.getLastKnownInputs();

    for (Rule &rule : rules)
    {
        if (rule.controlChannel.has_value() && !inputChannels.contains(*rule.controlChannel))
        {
            // The control channel is not active.
            continue;
        }

        ItemStats &itemStats = getItemStats(stack);
        uint64_t totalCount = itemStats.sentToNeg + itemStats.sentToPos + itemStats.sentToDef;
        if ((totalCount % rule.interval) != 0)
        {
            // The count indicates that we are not at the end of the interval.
            continue;
        }

        if (!rule.refStack.canMergeFrom(stack))
        {
            // Not matching the reference stack.
            continue;
        }

        // This rule matches.
        return rule.outputBelt;
    }

    return OutputBelt::Def;
}

Side MBeltJunction::getOutputSideFor(OutputBelt outputBelt)
{
    if (outputBelt == OutputBelt::Def)
    {
        return getInputSide().opposite();
    }

    int axis = getInputSide().axis() ^ 1;
    if (outputBelt == OutputBelt::Neg)
    {
        return Side(axis << 1);
    }
    else
    {
        return Side((axis << 1) | 1);
    }
}

MBeltJunction::ItemStats& MBeltJunction::getItemStats(ItemStack &stack)
{
    for (ItemStats &itemStats : stats.itemStats)
    {
        if (itemStats.refStack.canMergeFrom(stack))
        {
            return itemStats;
        }
    }

    size_t index = stats.itemStats.size();
    stats.itemStats.push_back(ItemStats{stack});
    return stats.itemStats[index];
}

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

int MBeltJunction::getRuleUpdateCounter()
{
    return ruleUpdateCounter;
}

std::vector<MBeltJunction::Rule> MBeltJunction::getRules()
{
    return rules;
}

void MBeltJunction::addRule()
{
    if (rules.size() >= maxRules)
    {
        return;
    }

    rules.push_back(Rule{std::nullopt, 1, ItemStack(), OutputBelt::Def});
    ruleUpdateCounter++;
}

void MBeltJunction::deleteRule(size_t index)
{
    if (index >= rules.size())
    {
        return;
    }

    rules.erase(rules.begin() + index);
    ruleUpdateCounter++;
}

void MBeltJunction::setRuleControlChannel(size_t index, std::optional<DataLink::Channel> controlChannel)
{
    if (index >= rules.size())
    {
        return;
    }

    rules[index].controlChannel = controlChannel;
    ruleUpdateCounter++;
}

void MBeltJunction::setRuleOutputBelt(size_t index, OutputBelt belt)
{
    if (index >= rules.size())
    {
        return;
    }

    rules[index].outputBelt = belt;
    ruleUpdateCounter++;
}

void MBeltJunction::setRuleInterval(size_t index, uint32_t interval)
{
    if (index >= rules.size())
    {
        return;
    }

    rules[index].interval = interval;
    ruleUpdateCounter++;
}

void MBeltJunction::setRuleRefStack(size_t index, ItemStack stack)
{
    if (index >= rules.size())
    {
        return;
    }

    rules[index].refStack = stack;
    ruleUpdateCounter++;
}

void MBeltJunction::swapRules(size_t a, size_t b)
{
    if (a >= rules.size() || b >= rules.size())
    {
        return;
    }

    Rule temp = rules[a];
    rules[a] = rules[b];
    rules[b] = temp;

    ruleUpdateCounter++;
}

int MBeltJunction::getStatUpdateCounter()
{
    return statUpdateCounter;
}

const MBeltJunction::Stats& MBeltJunction::getStats()
{
    return stats;
}

void MBeltJunction::serializeState(std::vector<uint8_t> &outBytes)
{
    serializeInto(outBytes, rules, stats);
}

void MBeltJunction::deserializeState(const uint8_t *data, size_t size)
{
    deserialize(data, size, rules, stats);
}

void MBeltJunction::saveMainConfig(MachineConfig &outConfig)
{
    outConfig.blockName = "main:beltJunction";
    outConfig.label = "Belt Junction";

    serializeInto(outConfig.components[""], rules);
}

void MBeltJunction::loadMainConfig(const MachineConfig &config, bool consumed)
{
    const auto &data = config.components.at("");
    deserialize(data.data(), data.size(), rules);
    ruleUpdateCounter++;
}

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

void MBeltJunction::onItemStackEntering(Side side, ItemStack &stack)
{
    if (side != getInputSide())
    {
        return;
    }

    ItemStats &itemStats = getItemStats(stack);
    OutputBelt outputBelt = getOutputBelt(stack);
    Side outputSide = getOutputSideFor(outputBelt);
    BeltComponent &beltComp = getComponent<BeltComponent>();
    Belt &belt = *beltComp.getOutputBelt(outputSide);
    Belt &defaultBelt = *beltComp.getOutputBelt(getOutputSideFor(OutputBelt::Def));

    belt.tryInsertAt(0, stack);

    if (!stack.isEmpty())
    {
        outputBelt = OutputBelt::Def;
        defaultBelt.tryInsertAt(0, stack);
    }

    if (stack.isEmpty())
    {
        stats.totalItemsProcessed++;
        if (outputBelt == OutputBelt::Neg)
        {
            itemStats.sentToNeg++;
        }
        else if (outputBelt == OutputBelt::Pos)
        {
            itemStats.sentToPos++;
        }
        else
        {
            itemStats.sentToDef++;
        }

        statUpdateCounter++;
    }
}

void MBeltJunction::getBeltsBranchingFrom(Side queriedSide, std::vector<std::shared_ptr<Belt>> &belts)
{
    BeltComponent &beltComp = getComponent<BeltComponent>();

    Side inputSide = getInputSide();
    if (queriedSide != inputSide)
    {
        return;
    }

    for (const Side &side : Side::all)
    {
        if (side.isZ())
        {
            continue;
        }

        if (side == inputSide)
        {
            continue;
        }

        belts.push_back(beltComp.getOutputBelt(side));
    }
}