#ifndef PLAYER_HPP_
#define PLAYER_HPP_

#include <ControllerChannel/ControllerChannel.hpp>
#include <ItemContainer/ItemContainer.hpp>
#include <ItemStore/ItemStore.hpp>
#include <Protocol/Protocol.hpp>
#include <Serialize/Utils.hpp>
#include <Utilities/CircularInt.hpp>
#include <Utilities/FixedPoint.hpp>
#include <array>
#include <unordered_map>
#include <vector>
#include <list>
#include <string>
#include <Utilities/Extern.hpp>
#include <BoundingBox/BoundingBox.hpp>
#include <Serialize/Serialize.hpp>
#include <ItemStack/ItemStack.hpp>
#include <Messenger/Messenger.hpp>
#include <Controller/Controller.hpp>
#include <Log/Log.hpp>
#include <Utilities/Flags.hpp>
#include <Item/ITool.hpp>
#include <Item/IInventory.hpp>
#include <Frustum/Frustum.hpp>
#include <AtomicDataStore/AtomicDataStore.hpp>
#include <UserDB/UserDB.hpp>
#include <VoxelSprite/VoxelSprite.hpp>
#include <Model/Model.hpp>
#include <AudioSource/AudioSource.hpp>
#include <Entity/PhysicsEntity/CameraEntity/CameraEntity.hpp>
#include <WorldView/WorldView.hpp>

/**
 * @brief Player entity, represents a player in a world.
 */
class Player : public CameraEntity
{
private:
    /**
     * @brief The special 'hand' tool; when no tool is used.
     */
    class HandTool : public ITool
    {
    public:
        virtual bool canDig(ItemStack &stack, Block &block) override;
        virtual float getPowerAgainst(ItemStack &stack, Block &block) override;
        virtual void harvest(ItemStack &stack, LocalWorld &world, glm::ivec3 pos) override;
        virtual Sound& getDigSoundFor(ItemStack &stack, Block &block) override;
    };

    static HandTool handToolInstance;

    AudioSource digAudioSource;
    
    Model& getCracks(int stage);

public:
    static constexpr float fullCircle = 2.0f * 3.1415f;
    static constexpr float accelerationCrouch = 0.03f;
    static constexpr float acceleration = 0.06f;
    static constexpr float accelerationSprintFactor = 1.5f;
    static constexpr float accelerationSwimXY = 0.06f;
    static constexpr float accelerationSwimZ = 0.02f;
    static constexpr float jumpVel = 0.21f;
    static constexpr float handRange = 8.0f;
    static constexpr float noclipVel = 0.6f;
    static constexpr float pickupRadius = 2.0f;
    static constexpr int numEquipmentSlots = 7;
    static constexpr int numInventorySlots = 50;
    static constexpr size_t maxChatLogLines = 16;
    static constexpr double headHeight = 2.5;
    static constexpr double crouchingHeadHeight = 1.875;
    static constexpr double eyeFromHeadOffset = 0.25;
    static constexpr double bodyRadius = 0.4;
    static constexpr double tagHeight = 3.0;
    static constexpr Uint32 typingTimeoutTicks = 3000;

    /**
     * @brief Control flags, indicating which actions the player is performing.
     */
    enum class ControlFlag
    {
        Left = 0,
        Right = 1,
        Forward = 2,
        Back = 3,
        Jump = 4,
        Dig = 5,
        Crouch = 6,
        Resync = 7,
        Sprint = 8,
        Jetpack = 9,
    };

    typedef Flags<ControlFlag> ControlFlags;

    enum class MessageType : uint32_t
    {
        RightClick,
        SetSelectedSlot,
        UpdateInventorySlot,
        StateUpdate,
        OpenInventory,
        UpdateDigState,
        SendChat,
        Drop,
        Achieved,
        OpenMap,
        UpdateMapPins,
        SetTypingStatus,
    };

private:
    UserDB::PermissionSet perms;
    
    struct DigState
    {
        int isDigging;
        glm::ivec3 pos;
        float currentDamage;
        float power;
        float hardness;

        DigState() : isDigging(0) {}
    };

    std::array<ControllerChannel*, Protocol::numControllerChannels> ctrlChannelTable = {};

    std::string m_name;
    int m_frameNum;
    ControllerChannel guiChannel;
    std::array<ControllerChannel, numEquipmentSlots> gadgetChannels;
    std::array<Item*, numEquipmentSlots> gadgetItems = {};
    DigState m_digState;
    VoxelSprite m_sprite;
    bool m_modelDirty;
    std::shared_ptr<Model> m_model;
    bool noclip = false;
    bool crouching = false;
    int lastFootstepDirection = -1;
    float walkingDistance = 4.0f;
    std::unordered_map<std::string, uint64_t> milestoneBitmaps;
    std::shared_ptr<Model> tagModel;
    glm::vec3 tagOffset;
    uint16_t nextStamp = 0;
    
    void wrapCamera();
    void sendSlotUpdate(const std::string &invId, int slot, ItemStack &stack);
    void sendDigUpdate();
    void updateCrouching();

    struct MessageHeader
    {
        MessageType type;
    };

    struct RightClickMessage
    {
        MessageType type;
        wfvec3 pos = wfvec3(0.0, 0.0, 0.0);
        wfvec3 dir = wfvec3(0.0, 0.0, 0.0);
    };

    struct SetSelectedSlotMessage
    {
        MessageType type;
        int slot;
    };

    struct StateUpdateMessage
    {
        MessageType type;
        wfvec3 pos = wfvec3(0.0, 0.0, 0.0);
        wfvec3 vel = wfvec3(0.0, 0.0, 0.0);
        float theta;
        float phi;
        ControlFlags controlFlags;
        bool noclip;
    };
    
    struct UpdateDigStateMessage
    {
        MessageType type;
        DigState digState;
    };

public:
    struct MapPin
    {
        std::string label;
        glm::ivec2 pos;
        bool switchedOn;
        std::array<uint8_t, 8> resv;        // Reserved for future expansion.

        inline bool operator==(const MapPin &other) const
        {
            return label == other.label && pos == other.pos && switchedOn == other.switchedOn;
        }
    };

    std::vector<std::string> chatHistory;
    std::atomic<bool> isBeingForcedOut;
    std::vector<MapPin> mapPins;
    bool gadgetsEnabled = true;
    bool isTyping = false;
    Protocol::ClientPrefs clientPrefs;

    /**
     * @brief Represents a chat entry.
     */
    struct ChatEntry
    {
        glm::vec3 color;
        std::string text;
        Uint32 receiveTime;
    };

    /**
     * @brief The list of chat entries.
     */
    std::list<ChatEntry> chatLog;

    ItemContainer items;
    ItemStore::iterator selectedSlot;

    /**
     * @brief The client messenger for this player.
     */
    std::shared_ptr<Messenger> client;

    /**
     * @brief The stack that's currently being moved in the GUI.
     */
    ItemStack movingStack;

    /**
     * @brief The current control flags.
     */
    ControlFlags controlFlags;

    /**
     * @brief The player's world view.
     */
    WorldView view;

    /**
     * @brief Construct the player.
     * 
     * @param world The world which the player is in.
     * @param typeId The type ID.
     * @param id The entity ID.
     * @param name The player name.
     */
    Player(World &world, std::string typeId, uint64_t id, std::string name = "");

    ~Player();

    glm::vec3 getForwardVector();
    glm::vec3 getRightVector();
    glm::vec3 getUpVector();
    float getThirdPersonViewDistance();
    bool areChunksLoaded();
    bool getNoclip();
    bool canDig(glm::ivec3 pos);
    bool isCrouching();
    void onRightClick();
    void onJumping();
    void onSetHUD(int num);
    void onOpenInventory();
    void onDrop();
    void onOpenMap();
    void updateNoclip();
    bool hasPermission(UserPermission perm);
    void teleport(glm::vec3 dest);
    void setNoclip(bool noclip);

    /**
     * @brief Returns true if the specified achievement in the specified milestone is completed.
     */
    bool hasCompletedAchievement(const std::string &milestoneId, int achievementId);

    /**
     * @brief Returns true if the specified achievement is currently reachable.
     */
    bool isAchievementReachable(const std::string &milestoneId, int achievementId);

    /**
     * @brief Returns true if all achievements in the specified milestone are completed.
     */
    bool hasCompletedMilestone(const std::string &milestoneId);

    /**
     * @brief Returns true if the specified milestone is currently reachable.
     */
    bool isMilestoneReachable(const std::string &milestoneId);

    /**
     * @brief Mark this player as having achieved the specified achievement.
     * 
     * This must be called on the server side (local world).
     */
    void achieve(const std::string &milestoneId, int achievementId);

    /**
     * @brief Get a reference to the currently-selected item stack.
     * 
     * @return The reference to the stack.
     */
    ItemStack& holdingStack();

    /**
     * @brief Return the index of the currently-selected HUD slot.
     */
    int getSelectedSlot();

    /**
     * @brief Get the player name.
     * 
     * @return The name.
     */
    std::string getName();

    /**
     * @brief Connect a controller.
     * 
     * This is ignored if a controller is already connected.
     * 
     * @tparam TController The controller class.
     * @tparam TArgs The argument types.
     * @param id The controller ID.
     * @param args Arguments to 'onConnect()'.
     */
    template<typename TController, typename... TArgs>
    void connectController(std::string id, TArgs... args)
    {
        auto &ctrl = static_cast<TController&>(guiChannel.attach(id, nextStamp++));
        ctrl.onConnect(args...);
    }

    /**
     * @brief Disconnect the controller.
     */
    void disconnectController();

    /**
     * @brief Send a player message.
     * 
     * @param type The message type.
     * @param payload The payload.
     * @param size The size of the payload.
     */
    void sendPlayerMessage(MessageType type, const uint8_t *payload, size_t size);

    /**
     * @brief Get the tool currently equipped by the player.
     * 
     * The returned tool instance has a static lifetime. A pointer is always returned:
     * if no tool is equipped, a special "hand" tool is returned.
     * 
     * @return A pointer to a tool instance.
     */
    ITool* getTool();

    /**
     * @brief Send a chat line to the server.
     * 
     * @param chat The chat line.
     */
    void sendChat(std::string chat);

    /**
     * @brief Returns true if this player is holding a screwdriver.
     */
    bool isHoldingScrewdriver();

    /**
     * @brief Get the glow color from the HUD items.
     */
    glm::vec3 getGlowColor();

    /**
     * @brief Update the digging state.
     * 
     * This is done on local worlds.
     */
    void updateDiggingState(LocalWorld &world);

    /**
     * @brief Draw the player model.
     */
    void drawModel(GraphicsContext &context, WorldDisplay &worldDisplay, Player &ourPlayer);

    /**
     * @brief Draw the block selector.
     */
    void drawBlockSelector(GraphicsContext &context, WorldDisplay &worldDisplay);

    /**
     * @brief Write a line to this player's chat log.
     * 
     * @param text The line of text.
     * @param color The color to use (white by default).
     */
    void writeChat(std::string text, glm::vec3 color = glm::vec3(1.0f));

    /**
     * @brief Returns the bounding box of the visible area of the world for this player.
     */
    BoundingBox<cint32> getViewBox();

    /**
     * @brief Pull fluid from equipments that provide fluid.
     */
    int pullFluidFromEquipment(std::string fluidName, int amount);

    void playFootstepSounds();
    void resetFootstepTimer();
    void onMapPinsUpdated();
    bool isGuiBusy();
    ControllerChannel* getControllerChannel(uint8_t channelId);
    void reattachGadget(size_t index);

    virtual void onMessage(const void *msg, size_t size) override;
    virtual void update() override;
    virtual void draw(GraphicsContext &context, WorldDisplay &display) override;
    virtual void serializeState(std::vector<uint8_t> &outBytes) override;
    virtual size_t deserializeState(const uint8_t *data, size_t size) override;
    virtual wfvec3 getEyePos() override;
    virtual BoundingBox<wfixed> getBoundingBox(wfvec3 pos) override;
    virtual glm::vec3 getFrictionCoefficients() override;
    virtual std::optional<bool> canGoTo(wfvec3 pos) override;
    virtual bool isUsingItemDirectly() override;
    virtual void onBlockPlaced(std::string blockName, glm::ivec3 pos) override;

    /**
     * @brief Overrides the default non-standard collision check for stair handling.
     * @return true if the player climbs stairs, false otherwise.
     */
    virtual bool onCollision() override;

    friend class Controller;

    friend class RemoteWorld;
    friend class ServerWorld;
    friend class LocalWorld;
    friend class SinglePlayerWorld;
    friend class WorldDisplay;
    friend class ControllerChannel;
    friend class GuiControllerView;
    friend class GrantCommand;
    friend class RevokeCommand;
    friend class AirMovementPhysics;
    friend class RemoteMap;
};

DERIVE_STATIC_CAST_SERIALIZER(Player::MessageType, uint32_t);

DERIVE_STRUCT_SERIALIZER(
    Player::MapPin,
    self.label,
    self.pos,
    self.switchedOn,
    self.resv
);

#endif