#ifndef LOCAL_WORLD_HPP_
#define LOCAL_WORLD_HPP_

#include <vector>
#include <unordered_map>
#include <string>
#include <memory>
#include <map>
#include <World/World.hpp>
#include <WorldGenerator/WorldGenerator.hpp>
#include <DataUpdatePlanner/DataUpdatePlanner.hpp>
#include <BeltManager/BeltManager.hpp>
#include <ChatCommand/ChatCommand.hpp>

/**
 * @brief Represents a local world (singleplayer, or the server side of a multiplayer world).
 */
class LocalWorld : public World
{
public:
    struct ScheduledSignal
    {
        glm::ivec3 pos;
        uint32_t code;
    };

    struct MachineTableEntry
    {
        int blockId;
        glm::ivec3 pos;
    };

protected:
    bool areCheatsEnabled;

private:
    static constexpr int maxSignalTicks = 60 * 60 * 24 * 7;

    bool allMachinesReady = false;

    std::unique_ptr<WorldGenerator> worldGenerator;

    std::vector<std::shared_ptr<Machine>> machines;
    std::vector<MachineTableEntry> machineTable;
    bool machineTableDirty = false;

    // next entity ID to use
    std::atomic<uint64_t> nextEntityID;

    std::atomic<uint64_t> nextBeltID;

    uint64_t gameTicks;
    std::map<uint64_t, std::vector<ScheduledSignal>> pendingSignals;

    volatile std::atomic<bool> isManagerBusy;
    Uint32 lastWriteTicks;

    std::vector<std::shared_ptr<Chunk>> chunksToSave;
    std::vector<std::shared_ptr<WorldGridNode>> nodesToUpdate[8];

    std::unordered_map<std::string, std::shared_ptr<ChatCommand>> commandTable;

    void saveChunks();
    void saveMachineTable();
    void saveMachines();
    void deleteMachineState(glm::ivec3 pos);
    void updateMachines();
    void updateEntities();
    void dispatchSignals();
    void commitPlan();
    void saveMiscDynamicData();
    void updateClocks();
    void saveDynamicState();
    void updateNodes();
    void registerGenericCommands();

protected:
    template<typename TCommand, typename... TArgs>
    void registerChatCommand(const std::string &cmdname, TArgs&&... args)
    {
        if (commandTable.contains(cmdname))
        {
            crash(formatString("Duplicate command '$+' registered.", cmdname));
        }

        commandTable[cmdname] = std::make_shared<TCommand>(args...);
    }

    /**
     * @brief Executes the specified command line.
     * 
     * @param player The player executing the command.
     * @param cmdline The command line.
     */
    void processCommand(Player &player, const std::string &cmdline);

    /**
     * @brief Called when the state of a BeltView has been changed.
     *
     * This does nothing for most world types, but for server worlds, it sends the updated
     * view to the clients.
     */
    virtual void onBeltViewUpdated(BeltView &view);

    /**
     * @brief Called when a belt view is removed.
     * 
     * This does nothing for most world types, but for server worlds, it sends a message to
     * notify clients that the view has been removed.
     */
    virtual void onBeltViewRemoved(uint64_t id);

    /**
     * @brief Called after a chunk has been loaded, and marked as loaded.
     *
     * This is called from inside the helper thread.
     */
    virtual void onChunkLoaded(std::shared_ptr<Chunk> chunk);

public:
    AtomicDataStore dataStore;
    DataUpdatePlanner planner;
    BeltManager beltManager;

    /**
     * @brief Construct a local world.
     * 
     * @param worldPath Path to the `.voxelverse` file.
     */
    LocalWorld(std::string worldPath);

    /**
     * @brief Allocate an entity ID.
     * 
     * Returns an entity ID which was not returned previously. This function is thread-safe.
     * 
     * @return The entity ID.
     */
    uint64_t allocEntityId();

    /**
     * @brief Allocate a belt ID.
     */
    uint64_t allocBeltId();

    /**
     * @brief Break and harvest the block at the specified position.
     * 
     * @param pos The block position.
     * @param notify Whether to notify neighbors.
     */
    void breakBlock(glm::ivec3 pos, bool notify = true);

    /**
     * @brief Attempt to spawn item entity in the players hand.
     */
    void dropItem(Player &player);

    /**
     * @brief Spawn an entity.
     * 
     * This can only be called on the server.
     * 
     * @tparam TEntity The entity type.
     * @tparam TArgs Constructor argument types.
     * @param args Additional arguments to the constructor.
     * @return The spawned entity.
     */
    template<typename TEntity, typename... TArgs>
    std::shared_ptr<TEntity> spawn(TArgs... args)
    {
        std::shared_ptr<TEntity> entity = std::make_shared<TEntity>(*this, allocEntityId(), args...);
        entities[entity->id] = entity;
        sendCreateEntityMessageToClients(entity);

        return entity;
    }

    /**
     * @brief Despawn the entity with the specified ID.
     * 
     * @param id The ID.
     */
    void despawnEntity(uint64_t id);

    /**
     * @brief Set the block at the specified coordinates.
     */
    bool setBlockAt(glm::ivec3 pos, int id, uint8_t meta = 0, bool notify = true);
    bool setBlockAt(glm::ivec3 pos, std::string name, uint8_t meta = 0, bool notify = true);

    /**
     * @brief Set the metadata of the specified block.
     * 
     * @param pos The block position.
     * @param meta Metadata value to store.
     * @param updateNeighbors If true, notify neighbors of this update (false by default).
     */
    bool setBlockMeta(glm::ivec3 pos, uint8_t meta, bool notifyNeighbors = false);

    /**
     * @brief Inform the neighbors of the specified block that it was updated.
     */
    void notifyNeighborsOf(glm::ivec3 pos);

    /**
     * @brief Inform the block (pos) that a neighbor on the Side s has changed. Send update signal.
     */
    void notifyAboutNeighbor(glm::ivec3 pos, Side side);

    /**
     * @brief Get the machine at the specified coordinates.
     * 
     * Returns `nullptr` if there is no machine.
     */
    std::shared_ptr<Machine> getMachine(glm::ivec3 pos);

    /**
     * @brief Send a signal to the specified block after the specified number of ticks.
     */
    void sendSignal(glm::ivec3 pos, uint32_t code, int numTicks);

    /**
     * @brief Cancel all signals pending for the specified block.
     */
    void cancelSignals(glm::ivec3 pos);

    /**
     * @brief Mark the specified chunk as needing to be saved.
     */
    void markNeedsSaving(std::shared_ptr<Chunk> chunk);

    /**
     * @brief Mark the specified node to be updated.
     */
    void markNodeForUpdate(std::shared_ptr<WorldGridNode> node);

    /**
     * @brief Called when a belt is updated.
     */
    void onBeltUpdated(const Belt &belt, bool isActive);

    /**
     * @brief Called when a belt with the specified ID is removed.
     */
    void onBeltRemoved(uint64_t id);

    /**
     * @brief Returns true if the specified block may be broken.
     */
    bool canBreak(glm::ivec3 pos);

    /**
     * @brief Get a player by user name, return `nullptr` if not in the game.
     */
    virtual std::shared_ptr<Player> getPlayer(const std::string &userName) = 0;

    virtual void onChunkRequested(std::shared_ptr<Chunk> chunk, float relativeDeadline) override;
    virtual void close() override;
    virtual void update() override;
    virtual bool isLocal() override;
    virtual void onPlayerUpdateBegins(Player &player) override;
    virtual void onPlayerMessage(Player &player, const void *msg, size_t size) override;
    virtual void writeDebugStats(std::ostream &os) override;
    virtual void writeBlockDebugInfo(std::ostream &os, glm::ivec3 pos) override;
    virtual bool canPurgeChunk(Chunk &chunk) override;
    virtual void onWorldGridNodeCreated(std::shared_ptr<WorldGridNode> node) override;
    virtual void getWorldStats(StatList &statList) override;
    virtual uint64_t getWorldTime() override;
    virtual std::shared_ptr<Messenger> getPeerMessenger(Player &player) override;

    friend class TerrainDumpCommand;
    friend class HelpCommand;
};

DERIVE_STRUCT_SERIALIZER(LocalWorld::ScheduledSignal, self.pos, self.code);
DERIVE_STRUCT_SERIALIZER(LocalWorld::MachineTableEntry, self.blockId, self.pos);

#endif