#ifndef MESSENGER_HPP_
#define MESSENGER_HPP_

#include <BoundingBox/BoundingBox.hpp>
#include <Utilities/CircularInt.hpp>
#include <Utilities/Utilities.hpp>
#include <concepts>
#include <string>
#include <memory>
#include <thread>
#include <Utilities/Socket.hpp>
#include <Protocol/Protocol.hpp>
#include <CommandQueue/CommandQueue.hpp>
#include <Utilities/Extern.hpp>
#include <Serialize/Serialize.hpp>
#include <vector>

class Entity;
class BeltView;
class Chunk;

/**
 * @brief Implements the message protocol on top of a socket.
 */
class Messenger
{
private:
    static constexpr int chunkDataCompressionLevel = 6;
    static constexpr size_t maxBlockUpdatesPerMessage = 0xFFFF / sizeof(Protocol::BlockUpdate);
    
    SOCKET m_socket;
    bool m_ok;
    std::thread m_thread;
    CommandQueue<int> m_queue;
    std::atomic<bool> disconnecting;
    BoundingBox<cint32> currentVisibleChunkBox;

    void threadMain();

public:
    /**
     * @brief Construct the messenger.
     * 
     * @param socket The socket. This function takes ownership of it.
     */
    Messenger(SOCKET socket);

    Messenger(const Messenger &) = delete;
    Messenger(Messenger &&) = delete;

    ~Messenger();

    template<
        std::predicate<Protocol::MessageType, const std::vector<uint8_t> &> MessageCallback
    >
    bool processIncoming(MessageCallback callback)
    {
        for (;;)
        {
            Protocol::MessageHeader header;
            uint16_t payloadSize;

            if (!readSocket(m_socket, (char*) &header, sizeof(header)))
            {
                return false;
            }

            std::memcpy(&payloadSize, header.payloadSize, 2);

            auto messageType = static_cast<Protocol::MessageType>(
                header.typeAndFlags &
                Protocol::MessageHeader::messageTypeMask
            );

            std::vector<uint8_t> rawPayload(payloadSize);
            if (!readSocket(m_socket, (char*) rawPayload.data(), rawPayload.size()))
            {
                return false;
            }

            if (header.typeAndFlags & Protocol::MessageHeader::compressed)
            {
                std::vector<uint8_t> payload;
                if (!uncompressBytes(rawPayload, payload))
                {
                    return false;
                }

                if (!callback(messageType, payload))
                {
                    return true;
                }
            }
            else if (!callback(messageType, rawPayload))
            {
                return true;
            }
        }
    }

    /**
     * @brief Disconnect the messenger.
     * 
     * The socket will be closed and further attempts to write will be prevented.
     */
    void disconnect();

    /**
     * @brief Send a message from raw data.
     * 
     * @param type The message type.
     * @param payload The payload.
     * @param size The size of the payload.
     */
    void sendRaw(Protocol::MessageType type, const void *payload, uint32_t size);

    /**
     * @brief Send a message with a plain struct as payload.
     * 
     * @tparam TPlain A plain struct type.
     * @param type The message type.
     * @param plain The plain struct.
     */
    template<typename TPlain>
    void sendPlain(Protocol::MessageType type, TPlain &&plain)
    {
        sendRaw(type, (void*) &plain, sizeof(TPlain));
    };

    /**
     * @brief Send a message made from serialized data.
     * 
     * @tparam TArgs Argument types.
     * @param type The type of message.
     * @param args The arguments to serialize.
     */
    template<typename... TArgs>
    void sendSerial(Protocol::MessageType type, TArgs... args)
    {
        std::vector<uint8_t> data = serialize(args...);
        sendRaw(type, data.data(), data.size());
    };

    /**
     * @brief Set the visible chunk box, and send an update if necessary.
     * 
     * This must only be called on the server side.
     */
    void setVisibleChunkBox(const BoundingBox<cint32> &visibleBox);

    /**
     * @brief Send a chunk to a client.
     *
     * If this chunk is outside the client's visible box, this method will filter it out.
     * 
     * @param chunk The chunk to send.
     */
    void sendChunk(Chunk &chunk);

    /**
     * @brief Send a list of block updates, breaking them up into multiple messages if necessary.
     */
    void sendBlockUpdates(const Protocol::BlockUpdate *updates, size_t numUpdates);

    /**
     * @brief Send an empty message (just type, no payload).
     * 
     * @param type The message type.
     */
    void sendEmpty(Protocol::MessageType type);

    /**
     * @brief Send a `KeepAlive` message from a client.
     * 
     * @param isPlayerTyping true if the player is currently typing.
     */
    void sendClientKeepAlive(bool isPlayerTyping);

    /**
     * @brief Send a protocol error message.
     * 
     * @param message The message.
     */
    void sendProtocolError(std::string message);

    /**
     * @brief Send an entity message.
     */
    void sendEntityMessage(uint64_t entityID, const void *msg, size_t size);

    /**
     * @brief Send a `CreateEntity` message.
     * 
     * @param entity The entity to be created.
     */
    void sendCreateEntityMessage(std::shared_ptr<Entity> entity);

    /**
     * @brief Send a `Chat` message.
     * 
     * @param text The text.
     * @param color The color.
     */
    void sendChat(const std::string &text, glm::vec3 color);

    /**
     * @brief Send a `PlaySound` message.
     * 
     * @param name The name of the sound to play.
     * @param blockPos The block position to play from.
     * @param vel The velocity of the sound source.
     */
    void sendPlaySoundMessage(const std::string &name, glm::ivec3 blockPos, glm::vec3 vel);

    /**
     * @brief Send an update of a BeltView.
     */
    void sendBeltViewUpdatedMessage(const BeltView &beltView);

    /**
     * @brief Send a message that a BeltView was removed.
     */
    void sendBeltViewRemovedMessage(uint64_t id);

    /**
     * @brief Send a "kicked from server" message with the specified reason.
     */
    void sendKickedFromServerMessage(std::string reason);

    /**
     * @brief Send client preferences to the server.
     */
    void sendSetClientPrefs(const Protocol::ClientPrefs &prefs);
};

#endif