#ifndef BELT_HPP_
#define BELT_HPP_

#include <list>

#include <ItemStack/ItemStack.hpp>
#include <Serialize/Serialize.hpp>
#include <Side/Side.hpp>

class LocalWorld;

/**
 * @brief Represents the serializable state of a belt.
 *
 * A belt has a starting position in block coordinates, a direction in which it
 * propagates, and a length in blocks. These properties identify its position in
 * the world.
 *
 * A belt is either operational or not. An operational belt has a source machine
 * such as a motor or item extractor, whereas a non-operational belt is disconnected
 * and nothing can push it. They need to be stored in case they are to be merged with
 * operational belts.
 *
 * The items on a belt are called "elements" and their positions are given using an
 * abstract coordinate space. When a belt is advanced, the beltOffset is incremented,
 * causing the whole space to shift relative to the world. Because unsigned 32-bit
 * integers are used to represent these, we can allow them to overflow, and we can
 * still convert them to world coordinates using (abstrctPos + beltOffset).
 */
class Belt
{
public:
    static constexpr int elementVoxelSpan = 4;

    struct Element
    {
        uint32_t abstractPos;
        ItemStack itemStack;
    };

    uint64_t id;

    glm::ivec3 startPos;
    Side dir;
    bool isOperational;

    uint32_t beltOffset = 0;
    uint32_t beltBlockLength = 1;
    std::list<Element> elements;

    uint64_t elementUpdateNum = 0;

    /**
     * @brief Move the belt forward by 1 voxel, if possible.
     */
    void advance(LocalWorld &world);

    /**
     * @brief Return the end position (one block beyond the edge of the belt).
     */
    glm::ivec3 getEndPos();

    /**
     * @brief If `pos` is a block on the belt, return its offset from the start in blocks.
     */
    std::optional<int> getBlockOffset(glm::ivec3 pos);

    /**
     * @brief Returns true if this belt holds any items.
     */
    bool hasItems();

    /**
     * @brief Return the of this conveyor belt.
     * 
     * This is based on its length and number of elements.
     */
    int getWeight();

    /**
     * @brief Try inserting a stack into the belt at the specified voxel offset.
     * 
     * @param voxelOffset The voxel offset where the item should be inserted.
     * @param stack The stack to insert (will be modified if items are removed from it).
     */
    void tryInsertAt(int voxelOffset, ItemStack &stack);

    /**
     * @brief Returns true if the belts are in equivalent states.
     * 
     * Note that the state of elements is compared using `elementUpdateNum`, not by
     * comparing the entire list. Furthermore, `beltOffset` is not compared.
     */
    bool operator==(const Belt &other) const;
};

DERIVE_STRUCT_SERIALIZER(
    Belt::Element, 
    self.abstractPos,
    self.itemStack
);

DERIVE_STRUCT_SERIALIZER(
    Belt,
    self.startPos,
    self.dir, 
    self.isOperational,
    self.beltOffset,
    self.beltBlockLength,
    self.elements
);

#endif