#ifndef MBELT_JUNCTION_HPP_
#define MBELT_JUNCTION_HPP_

#include <DataLink/DataLink.hpp>
#include <Item/Item.hpp>
#include <Machine/Machine.hpp>
#include <Machine/IElectricMachine.hpp>
#include <Machine/IBeltMachine.hpp>
#include <Machine/IDataMachine.hpp>
#include <FilterType/FilterType.hpp>
#include <ItemFilter/ItemFilter.hpp>
#include <Serialize/Serialize.hpp>
#include <memory>
#include <optional>

/**
 * @brief The machine for the belt junction block.
 */
class MBeltJunction :
    public Machine,
    public IBeltMachine,
    public IDataMachine
{
public:
    enum class OutputBelt
    {
        Neg = 0,
        Pos = 1,
        Def = 2,
    };

    /**
     * @brief Describes a filtering rule.
     */
    struct Rule
    {
        /**
         * @brief The rule only applies if this channel is active (`std::nullopt` means always applies).
         */
        std::optional<DataLink::Channel> controlChannel;

        /**
         * @brief The interval between instances of this item that this rule applies to.
         * 
         * E.g. if the value is 2, then every 2nd item is affected.
         */
        uint32_t interval;

        /**
         * @brief The reference stack to determine which item the rule applies to.
         *
         * An empty reference stack means the rule applies to all items.
         */
        ItemStack refStack;

        /**
         * @brief The belt that a matching item should be placed on.
         */
        OutputBelt outputBelt;
    };

    /**
     * @brief Describes the stats for a specific item.
     */
    struct ItemStats
    {
        ItemStack refStack;
        uint64_t sentToNeg = 0;
        uint64_t sentToPos = 0;
        uint64_t sentToDef = 0;
    };

    /**
     * @brief Encapsulates belt junction stats.
     */
    struct Stats
    {
        std::vector<ItemStats> itemStats;
        uint64_t totalItemsProcessed = 0;
    };

private:
    std::vector<Rule> rules;
    int ruleUpdateCounter = 0;
    Stats stats;
    int statUpdateCounter = 0;

    Side getInputSide();
    OutputBelt getOutputBelt(ItemStack &stack);
    Side getOutputSideFor(OutputBelt belt);
    ItemStats& getItemStats(ItemStack &stack);

public:
    static constexpr int maxRules = 32;
    static constexpr uint32_t maxInterval = 8;

    MBeltJunction(LocalWorld &world, glm::ivec3 pos);

    /**
     * @brief Return the current value of the rule update counter.
     */
    int getRuleUpdateCounter();

    /**
     * @brief Returns the list of rules.
     */
    std::vector<Rule> getRules();

    /**
     * @brief Add a new rule to the list, which initially has no effect.
     */
    void addRule();

    /**
     * @brief Delete the rule with the specified index.
     */
    void deleteRule(size_t index);

    /**
     * @brief Set the control channel of the specified rule.
     */
    void setRuleControlChannel(size_t index, std::optional<DataLink::Channel> controlChannel);

    /**
     * @brief Set the output belt of the specified rule.
     */
    void setRuleOutputBelt(size_t index, OutputBelt belt);

    /**
     * @brief Set the interval for the specified rule.
     */
    void setRuleInterval(size_t index, uint32_t interval);

    /**
     * @brief Set the reference stack of the specified rule.
     */
    void setRuleRefStack(size_t index, ItemStack stack);

    /**
     * @brief Swap the rules with the 2 specified indices.
     */
    void swapRules(size_t a, size_t b);

    /**
     * @brief Get the current value of the stat update counter.
     */
    int getStatUpdateCounter();

    /**
     * @brief Get a const reference to the stats.
     */
    const Stats& getStats();

    virtual void serializeState(std::vector<uint8_t> &outBytes) override;
    virtual void deserializeState(const uint8_t *data, size_t size) override;
    virtual void saveMainConfig(MachineConfig &config) override;
    virtual void loadMainConfig(const MachineConfig &config, bool consumed) override;
    virtual Side getDataInputSide() override;
    virtual void onItemStackEntering(Side side, ItemStack &stack) override;
    virtual void getBeltsBranchingFrom(Side side, std::vector<std::shared_ptr<Belt>> &belts) override;
};

DERIVE_STATIC_CAST_SERIALIZER(MBeltJunction::OutputBelt, uint8_t);

DERIVE_STRUCT_SERIALIZER(
    MBeltJunction::Rule,
    self.controlChannel,
    self.interval,
    self.refStack,
    self.outputBelt
);

DERIVE_STRUCT_SERIALIZER(
    MBeltJunction::ItemStats,
    self.refStack,
    self.sentToNeg,
    self.sentToPos,
    self.sentToDef
);

DERIVE_STRUCT_SERIALIZER(
    MBeltJunction::Stats,
    self.itemStats,
    self.totalItemsProcessed
);

#endif