#ifndef WORLD_HPP_
#define WORLD_HPP_

#include <Utilities/CircularInt.hpp>
#include <Utilities/FixedPoint.hpp>
#include <WeatherManager/WeatherManager.hpp>
#include <memory>
#include <shared_mutex>
#include <unordered_set>
#include <vector>
#include <optional>
#include <deque>
#include <Utilities/Extern.hpp>
#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <Protocol/Protocol.hpp>
#include <Messenger/Messenger.hpp>
#include <GeneratorNode/GeneratorNode.hpp>
#include <Frustum/Frustum.hpp>
#include <Utilities/Async.hpp>
#include <Side/Side.hpp>
#include <WorldGrid/WorldGrid.hpp>
#include <BoundingBox/BoundingBox.hpp>
#include <ChunkHandle/ChunkHandle.hpp>
#include <Sound/Sound.hpp>
#include <BeltView/BeltView.hpp>

class Machine;
class Block;
class GraphicsContext;
class WorldDisplay;
class ChunkWindow;

/**
 * @brief Represents a world.
 */
class World
{
private:
    struct ViewInfo
    {
        bool inUse;
        std::optional<BoundingBox<cint32>> lastKnownBox;
    };

    static constexpr int maxChunksPurgedPerUpdate = 64;

    std::vector<Block*> blocks;
    GeneratorNode randomGenerator;
    WorldGrid worldGrid;
    std::atomic<int> numLoadedChunks;
    std::vector<ViewInfo> viewInfos;
    std::deque<std::shared_ptr<Chunk>> purgeQueue;
    int currentBookKeepSeqNo = 0;

    bool isChunkReadyToDestroy(Chunk &chunk, int thisSeqNo);

protected:
    /**
     * @brief The current fatal error.
     * 
     * If this is the empty string, no fatal error has occured. Otherwise it indicates the error
     * message.
     */
    std::string fatalError;

    /**
     * @brief The entity table.
     */
    std::unordered_map<uint64_t, std::shared_ptr<Entity>> entities;

    /**
     * @brief The belt view table.
     */
    std::unordered_map<uint64_t, BeltView> beltViews;
    std::unordered_map<uint64_t, uint32_t> beltViewSpriteIds;
    std::shared_mutex beltViewMutex;

    /**
     * @brief The current time of day.
     */
    int timeOfDay;

    /**
     * @brief Maps block IDs to names.
     */
    std::vector<std::string> blockTable;

    /**
     * @brief Called by a subclass when the block table has been loaded.
     */
    void onBlockTableLoaded(const std::vector<std::string> &table);

    /**
     * @brief Advance the active belt views.
     */
    void advanceBeltViews();

    /**
     * @brief Assign a sprite ID to the specified belt view ID.
     *
     * Only call this from the main thread and only if you are holding an exclusive lock
     * on the belt view table.
     */
    void assignBeltViewSprite(uint64_t id);

    /**
     * @brief Release the sprite of the specified belt view ID.
     *
     * Only call this from the main thread and only if you are holding an exclusive lock
     * on the belt view table.
     */
    void eraseBeltViewSprite(uint64_t id);

    /**
     * @brief Called whenever the block grid is updated.
     * 
     * Does nothing by default.
     */
    virtual void onBlockGridUpdated(glm::ivec3 pos, int id, uint8_t meta);

    /**
     * @brief Begin the process of loading a chunk.
     * 
     * This method starts an asynchronous task of loading the chunk data (downloading it from server,
     * or loading from disk, or generating it, etc). The chunk passed in has `loadingInProgress` set to
     * `true`; once the chunk is loaded, the main thread (usually from a command) must set this to `false`,
     * and store the chunk data.
     * 
     * @param chunk The chunk to be loaded.
     * @param relativeDeadline The relative deadline.
     */
    virtual void onChunkRequested(std::shared_ptr<Chunk> chunk, float relativeDeadline) = 0;

public:
    static constexpr int cycleDuration = 60 * 60 * 24;

    typedef std::vector<std::pair<std::string, Widget*>> StatList;

    std::atomic<bool> shuttingDown;
    std::unique_ptr<ChunkWindow> window;
    WeatherManager weather;
    Uint32 lastTypingTicks = 0;
    std::unordered_set<std::string> typingPlayerNames;
    uint64_t typingPlayerUpdateCount = 0;

    struct BlockHit
    {
        glm::uvec3 coords;
        wfvec3 collisionPoint = wfvec3(0.0, 0.0, 0.0);
        Side side;
        RGBABytes collisionKey;
        bool hasHit;
    };

    /**
     * @brief Construct a world.
     */
    World();

    virtual ~World() = default;

    /**
     * @brief Perform sanity checks after shutting down a world.
     * 
     * This method will make sure that the world is in a state it should be in after it
     * has been shut down. If it is not, the game crashes.
     */
    void doShutdownSanityChecks();

    /**
     * @brief Get the chunk at the specified base position.
     * 
     * @param base The chunk base.
     * @return Shared pointer to the chunk, or `nullptr` if the chunk is not loaded.
     */
    std::shared_ptr<Chunk> getChunk(glm::ivec3 base, std::shared_ptr<WorldGridNode> hint = nullptr);

    /**
     * @brief Request a chunk.
     */
    ChunkHandle requestChunk(glm::ivec3 base, std::shared_ptr<WorldGridNode> hint = nullptr, float relativeDeadline = 0.0f);

    /**
     * @brief Get a reference to the block with the specified ID.
     * 
     * @param id The block ID.
     * @return A reference to the block, or a reference to the air block if id is invalid.
     */
    Block& getBlockById(int id);

    /**
     * @brief Get the block at the specified coordinates.
     * 
     * Returns `nullptr` if the chunk containing this block is not currently loaded.
     */
    Block* getBlockAt(glm::ivec3 pos);

    /**
     * @brief Get the metadata byte at the specified block.
     * 
     * Returns `std::nullopt` if the chunk containing this block is not currently loaded.
     */
    std::optional<uint8_t> getBlockMeta(glm::ivec3 pos);

    /**
     * @brief See if the ray hits a block.
     * 
     * @param start The start position of the ray.
     * @param dir The direction of the ray (length must be 1).
     * @param maxDistance The maximum distance.
     * @return BlockHit The block hit.
     */
    BlockHit castRay(wfvec3 start, wfvec3 dir, double maxDistance);

    /**
     * @brief Close the world.
     * 
     * This is called when the player exits the world.
     */
    virtual void close() = 0;

    /**
     * @brief Called on each 'fast-update'.
     *
     * @param chunkWindow `nullptr` on server.
     */
    virtual void fastUpdate(ChunkWindow *chunkWindow) = 0;

    /**
     * @brief Called on each update.
     */
    virtual void update() = 0;

    /**
     * @brief Returns true if the world is ready to shut down.
     */
    virtual bool isShutdownReady();

    /**
     * @brief Send a message to an entity.
     * 
     * @param id The entity ID.
     * @param msg The message.
     * @param size The size.
     */
    virtual void sendMessageToEntity(uint64_t id, const void *msg, size_t size) = 0;

    /**
     * @brief Return a reference to the block table.
     */
    std::vector<std::string>& getBlockTable();

    /**
     * @brief Return true if we are on a local world.
     */
    virtual bool isLocal() = 0;

    /**
     * @brief Generate and return a random value.
     */
    GeneratorNode::ID generateRandom();

    /**
     * @brief Returns true with `change`% probability.
     * 
     * @param chance The chance (in %, out of 100).
     */
    bool probably(int chance);

    /**
     * @brief Perform housekeeping operations.
     */
    void housekeep();

    /**
     * @brief Dump the purge queue to the log.
     */
    void dumpPurgeQueue();

    /**
     * @brief Draw the belts in this world.
     */
    void drawBelts(WorldDisplay &worldDisplay, const Frustum &viewingFrustum);

    /**
     * @brief Update the belt animations.
     */
    void updateBeltAnimations(WorldDisplay &worldDisplay, const Frustum &viewingFrustum);

    /**
     * @brief Mark a chunk as dirty (needing re-render).
     * 
     * @param base The chunk base.
     */
    void markChunkDirty(glm::ivec3 base);

    /**
     * @brief Get the ID of the block with the specified internal name.
     */
    int getBlockId(std::string name);

    /**
     * @brief Return true if the chunk containing the specified block is loaded.
     */
    bool isChunkLoaded(glm::ivec3 pos);

    /**
     * @brief Return the number of loaded chunks.
     * 
     * This includes chunks which are cached.
     */
    int getNumLoadedChunks();

    /**
     * @brief Get the current time of day.
     */
    int getTimeOfDay();

    /**
     * @brief Set the time of day.
     */
    void setTimeOfDay(int time);

    /**
     * @brief Get the solar angle based on time of day.
     * 
     * @return The solar angle in radians.
     */
    float getSolarAngle();

    /**
     * @brief Returns the intensity of the sun, based on the time of day, weather, etc.
     */
    float getSunIntensity();

    /**
     * @brief Get the intensity of the player's mini-light, depending on time of day.
     */
    float getPlayerLightIntensity(Player &player);

    /**
     * @brief Get the sky color based on the time of day.
     */
    glm::vec3 getSkyColor();

    /**
     * @brief Get the color of ambient light based on the time of day.
     */
    glm::vec3 getAmbientColor();

    /**
     * @brief Return the fatal error message if in fatal state, empty string otherwise.
     */
    std::string getFatalError();

    /**
     * @brief Delivery a message to an entity.
     * 
     * @param id The entity.
     * @param msg The message.
     * @param size The message size.
     */
    void deliverEntityMessage(uint64_t id, const void *msg, size_t size);

    /**
     * @brief Called when a player update begins.
     * 
     * @param player The player.
     */
    virtual void onPlayerUpdateBegins(Player &player);

    /**
     * @brief Called when a player update is over.
     * 
     * This saves the player state on server and singleplayer worlds, or sends a client update on
     * remote worlds.
     * 
     * @param player The player.
     */
    virtual void onPlayerUpdateOver(Player &player) = 0;

    /**
     * @brief Call a function on every entity.
     * 
     * @param callback The callback.
     */
    void forEachEntity(std::function<void(std::shared_ptr<Entity>)> callback);

    /**
     * @brief Send a `CreateEntity` message to all clients.
     * 
     * @param entity The entity which was created.
     */
    virtual void sendCreateEntityMessageToClients(std::shared_ptr<Entity> entity) = 0;

    /**
     * @brief Send a `DeleteEntity` message to all clients.
     * 
     * @param id The entity ID.
     */
    virtual void sendDeleteEntityMessageToClients(uint64_t id) = 0;

    /**
     * @brief Draw the specified player's model according to the rules in this world.
     * 
     * By default does nothing. Only in `RemoteWorld` it draws the player model for every player
     * which is not our player.
     * 
     * @param player The player.
     */
    virtual void drawPlayerModel(GraphicsContext &context, WorldDisplay &worldDisplay, Player &player);

    /**
     * @brief Draw a block selector according to the rules in this world.
     * 
     * By default does nothing. In `RemoteWorld` and `SinglePlayerWorld`, draws the block selector
     * only if this is our player.
     * 
     * @param player The player.
     */
    virtual void drawBlockSelector(GraphicsContext &context, WorldDisplay &worldDisplay, Player &player);

    /**
     * @brief Called when a player receives a non-universal message.
     */
    virtual void onPlayerMessage(Player &player, const void *msg, size_t size) = 0;

    /**
     * @brief Output debug stats to the specified stream.
     * 
     * This is called by the F3 menu. By default it does nothing.
     * 
     * @param os The stream to output to.
     */
    virtual void writeDebugStats(std::ostream &os);

    /**
     * @brief Output extra debug info about a block to the specified stream.
     *
     * This is called by the F3 menu when a block is selected. By default is does
     * nothing.
     * 
     * @param os The stream to output to.
     * @param pos The block position.
     */
    virtual void writeBlockDebugInfo(std::ostream &os, glm::ivec3 pos);

    /**
     * @brief Return true if it is acceptable to purge the specified chunk.
     */
    virtual bool canPurgeChunk(Chunk &chunk) = 0;

    /**
     * @brief Allocate and return a new view index.
     */
    size_t allocView();

    /**
     * @brief Declare the view index as no longer used.
     */
    void deallocView(size_t index);

    /**
     * @brief Set the retain box for a view.
     * 
     * @param viewIndex The view index.
     * @param box The retain box.
     * @param onNewlyVisible Called on every chunk that has become newly visible.
     */
    void setRetainBox(
        size_t viewIndex,
        const BoundingBox<cint32> &box,
        const std::function<void(Chunk&)> &onNewlyVisible
    );

    /**
     * @brief Enqueue the specified chunk for purge.
     * 
     * @param chunk The chunk to purge.
     */
    void enqueueForPurge(std::shared_ptr<Chunk> chunk);

    /**
     * @brief Return the number of chunks in the purge queue.
     */
    size_t getPurgeQueueSize();

    /**
     * @brief Begin the process of loading a chunk.
     */
    void beginChunkLoad(std::shared_ptr<Chunk> chunk, float relativeDeadline);

    /**
     * @brief Called when a world grid node is created.
     */
    virtual void onWorldGridNodeCreated(std::shared_ptr<WorldGridNode> node) = 0;

    /**
     * @brief Return the root node.
     */
    std::shared_ptr<WorldGridNode> getRootNode();

    /**
     * @brief Play the specified sound.
     * 
     * On servers, this requests the sound to be played on all clients. On single-player
     * worlds, it simply plays the sound. On remote worlds this is not valid.
     */
    virtual void playSound(Sound &sound, glm::ivec3 blockPos, glm::vec3 vel = glm::vec3(0.0f)) = 0;

    /**
     * @brief Called when a physics entity is walking over a belt at `pos`.
     */
    void onWalkingOverBelt(glm::ivec3 pos, PhysicsEntity &entity);

    /**
     * @brief Get the sprite ID corresponding to belt at the specified position.
     */
    uint64_t getBeltSprite(glm::ivec3 pos);

    /**
     * @brief Called when the chunk window is set.
     */
    void onWindowInitDone();

    /**
     * @brief Get a list of stats for this world.
     */
    virtual void getWorldStats(StatList &statList);

    /**
     * @brief Get the effective world time - the number of updates since t=0.
     */
    virtual uint64_t getWorldTime() = 0;

    /**
     * @brief Returns the text describing which players are currently typing.
     */
    std::string getTypingIndicatorString();

    /**
     * @brief Returns the peer messenger for the specified player, if there is one.
     */
    virtual std::shared_ptr<Messenger> getPeerMessenger(Player &player) = 0;
};

#endif