#ifndef WIRING_HPP_
#define WIRING_HPP_

#include <cstdint>
#include <vector>
#include <unordered_map>
#include <optional>

#include <WorldComponent/WorldComponent.hpp>
#include <Utilities/Extern.hpp>
#include <Side/Side.hpp>
#include <Serialize/Serialize.hpp>
#include <WorldGridNode/WorldGridNode.hpp>

/**
 * @brief Base class for wiring components (e.g. for energy).
 */
class Wiring : public WorldComponent
{
public:
    /**
     * @brief The absolute limit past which wires will not connect.
     */
    static constexpr uint32_t wireDistanceLimit = UINT32_C(1) << 30;

    /**
     * @brief Describe a connection between ports.
     */
    struct Connection
    {
        /**
         * @brief The target port index within the cell.
         */
        uint64_t targetPort;

        /**
         * @brief The shortest-path distance.
         */
        uint32_t distance;
    };

    /**
     * @brief Describes a port, where a node connects to other objects.
     */
    struct Port
    {
        /**
         * @brief The position of the port, in world coordinates.
         */
        glm::ivec3 pos;

        /**
         * @brief Which side of the block indicated by `pos` is an exit from this node.
         */
        Side side;

        /**
         * @brief The list of connections.
         */
        std::vector<Connection> connections;

        /**
         * @brief The machine to which we redirect (by default `pos + side.out()`).
         */
        glm::ivec3 machinePos;

        /**
         * @brief The ID of the last visitor of this port.
         */
        uint64_t lastVisitorId;

        /**
         * @brief The distance travelled by the last visitor.
         */
        uint32_t visitorDistance;
    };

private:
    typedef std::unordered_map<uint16_t, std::vector<Port>> CellTable;

    class GraphWalker
    {
    public:
        std::vector<Port> &ports;
        uint64_t startingPort;
        std::vector<Connection> connectionsFound;

        GraphWalker(std::vector<Port> &ports, uint64_t startingPort);

        std::optional<uint64_t> findPortIndex(glm::ivec3 pos, Side side);
        void onConnectionFound(uint64_t targetPort, uint32_t distance);
        bool compareConnections();
        bool compareAndUpdateConnections();
        
        static std::optional<uint64_t> findPortIndex(glm::ivec3 pos, Side side, std::vector<Port> &ports);
    };

    class ChunkWalker : public GraphWalker
    {
    private:
        Chunk &chunk;
        uint16_t shortestDistanceMap[16 * 16 * 16];

        void walkRecur(glm::ivec3 pos, Side sourceSide, uint16_t distance);

    public:
        ChunkWalker(Chunk &chunk, std::vector<Port> &ports, uint64_t startingPort);

        void run();
    };

    class BranchWalker : public GraphWalker
    {
    private:
        WorldGridNode &node;
        CellTable &cells;
        uint64_t visitorId;

        void walkRecur(glm::ivec3 pos, Side side, uint32_t distance);
    
    public:
        BranchWalker(
            WorldGridNode &node,
            CellTable &cells,
            std::vector<Port> &ports,
            uint64_t startingPort,
            uint64_t visitorId
        );

        void run();
    };

    CellTable cells;
    bool needsReroute = false;
    uint64_t nextVisitorId = 1;

    void rerouteLeaf(LocalWorld &world, std::shared_ptr<WorldGridNode> node);
    void rerouteBranch(LocalWorld &world, std::shared_ptr<WorldGridNode> node);

protected:
    /**
     * @brief Global machine ports, only used for the root node.
     */
    std::vector<Port> globalPorts;

    /**
     * @brief Called when the list of machine ports changes.
     * 
     * @param ports The current list of ports.
     */
    virtual void onMachinePortsChanged(LocalWorld &world, std::shared_ptr<WorldGridNode> node, std::vector<Port> &ports) = 0;

    /**
     * @brief Returns true if the specified block can be walked.
     */
    virtual bool isWalkableBlock(LocalWorld &world, int id) = 0;

    /**
     * @brief Returns the name to be used by 
     */
    virtual std::string getDumpDirName() = 0;

public:
    Wiring(const uint8_t *data, size_t size);

    /**
     * @brief Indicates that the routes on this node may have changed.
     */
    void requestReroute(LocalWorld &world, std::shared_ptr<WorldGridNode> node);

    /**
     * @brief Called when a wire of this type is placed in the world.
     */
    void onWireBlockPlaced(LocalWorld &world, std::shared_ptr<WorldGridNode> node, glm::ivec3 pos);

    /**
     * @brief Called when a wire of this type is removed from the world.
     */
    void onWireBlockRemoved(LocalWorld &world, std::shared_ptr<WorldGridNode> node, glm::ivec3 pos);

    /**
     * @brief Called when the specified side of a machine becomes connectable.
     */
    void onMachineSidePlaced(LocalWorld &world, std::shared_ptr<WorldGridNode> node, glm::ivec3 pos, Side side);

    /**
     * @brief Called when the specified side of a machine becomes connectable, and which machine it redirects to.
     */
    void onMachineSidePlaced(
        LocalWorld &world,
        std::shared_ptr<WorldGridNode> node,
        glm::ivec3 pos,
        Side side,
        glm::ivec3 machinePos
    );

    /**
     * @brief Called when the specified side of a machine becomes unconnectable.
     */
    void onMachineSideRemoved(LocalWorld &world, std::shared_ptr<WorldGridNode> node, glm::ivec3 pos, Side side);

    virtual void serializeState(std::vector<uint8_t> &outBytes) override;
    virtual bool update(LocalWorld &world, std::shared_ptr<WorldGridNode> node) override;
    virtual void dump(std::shared_ptr<WorldGridNode> node) override;
};

DERIVE_STRUCT_SERIALIZER(Wiring::Port, self.pos, self.side, self.connections, self.machinePos);
DERIVE_STRUCT_SERIALIZER(Wiring::Connection, self.targetPort, self.distance);

#endif