#ifndef WORLD_DISPLAY_HPP_
#define WORLD_DISPLAY_HPP_

#include <AudioSource/AudioSource.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Vk/DescriptorSetLayout/DescriptorSetLayout.hpp>
#include <unordered_map>
#include <Display/WidgetDisplay/WidgetDisplay.hpp>
#include <World/World.hpp>
#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <Vk/Image/Image.hpp>
#include <Vk/ImageView/ImageView.hpp>
#include <Vk/Framebuffer/Framebuffer.hpp>
#include <Vk/RenderPass/RenderPass.hpp>
#include <Vk/Pipeline/Pipeline.hpp>
#include <Vk/DescriptorPool/DescriptorPool.hpp>
#include <Vk/DescriptorSet/DescriptorSet.hpp>
#include <Vk/Buffer/Buffer.hpp>
#include <FrameDrawer/FrameDrawer.hpp>
#include <InputManager/InputManager.hpp>
#include <any>

class VoxelTree;

/**
 * @brief The world display; for displaying the main game.
 */
class WorldDisplay : public WidgetDisplay
{
private:
    static constexpr size_t chatMax = 15;

    struct ModelData
    {
        alignas(16) glm::mat4 matrix;
        alignas(16) glm::vec3 spread;
        uint32_t localNoiseValue;
    };

    static std::unordered_map<Material*, std::shared_ptr<Model>> materialModels;
    static const char *gpuDebugModeNames[];

    std::unordered_map<std::string, std::any> sessionCache;

    std::shared_ptr<World> world;

    std::shared_ptr<Player> player;
    std::vector<Widget*> gadgets;

    bool shouldCloseMenu = false;
    bool thirdPerson = false;
    bool showDebugInfo = false;
    bool showFrameHistory = false;
    bool showPlayerList = false;

    int lastKnownFramebufferResolution;
    VkExtent2D lastKnownExtent;
    VkExtent2D framebufferExtent;

    std::unique_ptr<FrameDrawer> frameDrawers[GraphicsContext::maxFramesInFlight];

    std::string chatToSend;

    AudioSource rainSource;

    void recreateGraphicsObjects(GraphicsContext &context);
    void prepareFrame(GraphicsContext &context);
    void drawOverlays(GraphicsContext &context);
    void setOverlayWidget(Widget *widget);
    void openChatOverlay();

public:
    static constexpr Uint32 chatFadeAfter = 3000;
    static constexpr Uint32 chatFadeDuration = 3000;
    static constexpr int gpuDebugModeLabelWidth = 100;
    static constexpr int gpuDebugModeLabelOffsetRight = 10;
    static constexpr int gpuDebugModeLabelOffsetTop = 5;
    static constexpr float gpuDebugModeOpacity = 0.8f;

    static std::shared_ptr<Vk::DescriptorSetLayout> worldModelDescriptorSetLayout;
    static std::shared_ptr<Vk::DescriptorSetLayout> modelDescriptorSetLayout;

    std::shared_ptr<Vk::ImageView> colorImageView;
    std::shared_ptr<Vk::DescriptorSetLayout> worldFrameDescriptorSetLayout;

    std::shared_ptr<Vk::Image> lightImage;
    std::shared_ptr<Vk::ImageView> lightImageView;
    std::shared_ptr<Vk::Image> normalImage;
    std::shared_ptr<Vk::Image> posSpecularImage;
    std::shared_ptr<Vk::Buffer> lightClusterBuffer;
    std::shared_ptr<Vk::DescriptorSetLayout> lightClusterDescSetLayout;
    std::shared_ptr<Vk::DescriptorSet> lightClusterDescSet;
    std::shared_ptr<Vk::Buffer> tileDepthBuffer;
    std::shared_ptr<Vk::DescriptorSet> tileDepthDescSet;
    std::shared_ptr<Vk::Pipeline> tileDepthPipeline;
    std::shared_ptr<Vk::Pipeline> lightClusterResetPipeline;
    std::shared_ptr<Vk::Pipeline> lightScopesPipeline;
    std::shared_ptr<Vk::Pipeline> lightCountPipeline;
    std::shared_ptr<Vk::ImageView> normalImageView;
    std::shared_ptr<Vk::ImageView> posSpecularImageView;
    std::shared_ptr<Vk::DescriptorSetLayout> lightApplyIrradianceDescSetLayout;
    std::shared_ptr<Vk::DescriptorSet> lightApplyIrradianceDescSet;
    std::shared_ptr<Vk::Pipeline> lightApplyPipeline;
    std::shared_ptr<Vk::DescriptorSetLayout> lightApplyGbufferDescSetLayout;
    std::shared_ptr<Vk::DescriptorPool> lightApplyGbufferDescPool;
    std::shared_ptr<Vk::DescriptorSet> lightApplyGbufferDescSet;

    std::shared_ptr<Vk::DescriptorSetLayout> tintDescriptorSetLayout;

    std::shared_ptr<Vk::RenderPass> particlesRenderPass;
    std::shared_ptr<Vk::Framebuffer> particlesFramebuffer;
    std::shared_ptr<Vk::Pipeline> particlesPipeline;

    std::shared_ptr<Vk::RenderPass> terrainRenderPass;
    std::shared_ptr<Vk::Framebuffer> terrainFramebuffer;
    std::shared_ptr<Vk::Pipeline> terrainPipeline;

    std::shared_ptr<Vk::RenderPass> modelRenderPass;
    std::shared_ptr<Vk::Framebuffer> modelFramebuffer;
    std::shared_ptr<Vk::Pipeline> modelPipeline;

    std::shared_ptr<Vk::RenderPass> combineRenderPass;
    std::shared_ptr<Vk::Pipeline> combinePipeline;
    std::shared_ptr<Vk::DescriptorSetLayout> skyboxDescriptorSetLayout;
    std::shared_ptr<Vk::DescriptorSetLayout> combineDescriptorSetLayout;
    std::shared_ptr<Vk::DescriptorPool> combineDescriptorPool;
    std::shared_ptr<Vk::DescriptorSet> combineDescriptorSet;

    InputManager inputManager;

    int gpuDebugMode;
    int debugSpecular = 127;
    bool enableDebugSpecular = false;
    int debugRefractValue = 0x1A;
    bool enableDebugRefractValue = false;
    glm::mat4 cloudTransform = glm::mat4(1.0f);
    glm::vec2 windSpeed = glm::vec2(glm::radians(0.01f));
    glm::vec2 targetWindSpeed = glm::vec2(glm::radians(0.01f));
    Uint32 windLastUpdateTicks = 0;

    /**
     * @brief Construct a world display with the specified chunk provider.
     * 
     * @param world The world.
     * @param player The player we control.
     */
    WorldDisplay(std::shared_ptr<World> world, std::shared_ptr<Player> player);

    ~WorldDisplay();

    /**
     * @brief Get the player that we are controlling.
     */
    std::shared_ptr<Player> getPlayer();

    /**
     * @brief Returns true if this world display is in third person view.
     */
    bool inThirdPersonView();

    /**
     * @brief Get the world.
     */
    std::shared_ptr<World> getWorld();

    /**
     * @brief Draw a material converted into a 3D mesh.
     */
    void drawMeshedIcon(GraphicsContext &context, Material &mat, glm::vec3 pos);

    /**
     * @brief Draw a material converted into a 3D mesh, laying flat like on a belt.
     */
    void drawMeshedIconFlat(GraphicsContext &context, Material &mat, glm::mat4 matrix);

    /**
     * @brief Draw a model inside the specified bounds.
     */
    void drawModelInBox(Model &model, const BoundingBox<wfixed> &bounds);

    /**
     * @brief Get the current frame drawer.
     */
    FrameDrawer& getFrameDrawer();

    /**
     * @brief Get a session object with the specified URL.
     * 
     * If the state does not yet exist, it is created using the default constructor. If you
     * call this method with different types on the same URL, it causes a crash.
     *
     * Session objects are destroyed when the world display is destroyed.
     */
    template<typename TState>
    std::shared_ptr<TState> getSessionObject(const std::string &url)
    {
        if (sessionCache.contains(url))
        {
            return std::any_cast<std::shared_ptr<TState>>(sessionCache[url]);
        }

        std::shared_ptr<TState> newState = std::make_shared<TState>();
        sessionCache[url] = newState;
        return newState;
    }

    void requestGuiClose();

    virtual void draw(GraphicsContext &context) override;
    virtual void update() override;
    virtual void fastUpdate() override;
    virtual void onEvent(SDL_Event &event) override;
    virtual void onQuit() override;

    static void initStaticObjects();

    friend class ItemEntity;
    friend class BlockItem;
    friend class ChunkRenderer;
    friend class BeltView;
    friend class FrameDrawer;
    friend class DrawStage;
    friend class Controller;
    friend class InputManager;
    friend class EscapeMenu;
};

#endif